mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-14 19:32:11 +03:00
Drag-n-drop update (#4220)
* Reuse `splitFilePath` function * unnecessary `width` and `height` removed * added utils for draggable file list items - added separate CSS * WIP new styles for the commit draggable * styles for draggable commit cards updated * Draggable hunk added * Draggable lanes updated * Dropzone design updated * Dropzones code refactor * reordering lines design update * Update logic for determining reorder shift * Remove unused CSS fix scrollable container prop * dropzone animations added * Dropzone hover state UI updated * CSS update: Card overlay labels * Fix: horizontal scroll wrong observer trigger * UX: Automatically close the commit message box after commit
This commit is contained in:
parent
a1c591ffb6
commit
9ff735fd4e
@ -139,9 +139,9 @@
|
||||
<PullRequestCard />
|
||||
|
||||
<div class="card">
|
||||
<Dropzones>
|
||||
{#if branch.files?.length > 0}
|
||||
<div class="branch-card__files">
|
||||
{#if branch.files?.length > 0}
|
||||
<div class="branch-card__files">
|
||||
<Dropzones>
|
||||
<BranchFiles
|
||||
files={branch.files}
|
||||
{isUnapplied}
|
||||
@ -162,21 +162,23 @@
|
||||
</InfoMessage>
|
||||
</div>
|
||||
{/if}
|
||||
</Dropzones>
|
||||
|
||||
{#if branch.active}
|
||||
<CommitDialog
|
||||
projectId={project.id}
|
||||
expanded={commitBoxOpen}
|
||||
hasSectionsAfter={branch.commits.length > 0}
|
||||
on:action={(e) => {
|
||||
if (e.detail === 'generate-branch-name') {
|
||||
generateBranchName();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if branch.commits.length === 0}
|
||||
{#if branch.active}
|
||||
<CommitDialog
|
||||
projectId={project.id}
|
||||
expanded={commitBoxOpen}
|
||||
hasSectionsAfter={branch.commits.length > 0}
|
||||
on:action={(e) => {
|
||||
if (e.detail === 'generate-branch-name') {
|
||||
generateBranchName();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if branch.commits.length === 0}
|
||||
<Dropzones>
|
||||
<div class="new-branch">
|
||||
<EmptyStatePlaceholder image={laneNewSvg} width="11rem">
|
||||
<svelte:fragment slot="title">This is a new branch</svelte:fragment>
|
||||
@ -185,7 +187,9 @@
|
||||
</svelte:fragment>
|
||||
</EmptyStatePlaceholder>
|
||||
</div>
|
||||
{:else}
|
||||
</Dropzones>
|
||||
{:else}
|
||||
<Dropzones>
|
||||
<div class="no-changes" data-dnd-ignore>
|
||||
<EmptyStatePlaceholder image={noChangesSvg} width="11rem" hasBottomMargin={false}>
|
||||
<svelte:fragment slot="caption"
|
||||
@ -193,8 +197,8 @@
|
||||
>
|
||||
</EmptyStatePlaceholder>
|
||||
</div>
|
||||
{/if}
|
||||
</Dropzones>
|
||||
</Dropzones>
|
||||
{/if}
|
||||
|
||||
<div class="card-commits">
|
||||
<CommitList {isUnapplied} />
|
||||
@ -254,9 +258,6 @@
|
||||
|
||||
.card {
|
||||
flex: 1;
|
||||
/* overflow: hidden; */
|
||||
/* border: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m); */
|
||||
}
|
||||
|
||||
.branch-card__files {
|
||||
@ -264,9 +265,6 @@
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
/* border-left: 1px solid var(--clr-border-2);
|
||||
border-right: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m) var(--radius-m) 0 0; */
|
||||
}
|
||||
|
||||
.card-notifications {
|
||||
@ -288,12 +286,6 @@
|
||||
cursor: default; /* was defaulting to text cursor */
|
||||
}
|
||||
|
||||
.branch-card :global(.contents) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* COLLAPSED LANE */
|
||||
.collapsed-lane-container {
|
||||
display: flex;
|
||||
|
@ -54,7 +54,7 @@
|
||||
options: {
|
||||
root: null,
|
||||
rootMargin: '-1px',
|
||||
threshold: 1
|
||||
threshold: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -131,6 +131,5 @@
|
||||
|
||||
.not-in-viewport {
|
||||
border-radius: 0;
|
||||
/* background-color: aquamarine; */
|
||||
}
|
||||
</style>
|
||||
|
@ -77,7 +77,7 @@
|
||||
{#await $selectedFile then selected}
|
||||
{#if selected}
|
||||
<div
|
||||
class="file-preview resize-viewport"
|
||||
class="file-preview"
|
||||
bind:this={rsViewport}
|
||||
in:slide={{ duration: 180, easing: quintOut, axis: 'x' }}
|
||||
style:width={`${fileWidth || $defaultFileWidthRem}rem`}
|
||||
|
@ -58,6 +58,5 @@
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
/* overflow: hidden; */
|
||||
}
|
||||
</style>
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import CommitMessageInput from '$lib/commit/CommitMessageInput.svelte';
|
||||
import { persistedCommitMessage } from '$lib/config/config';
|
||||
import { draggable } from '$lib/dragging/draggable';
|
||||
import { draggableCommit } from '$lib/dragging/draggable';
|
||||
import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables';
|
||||
import BranchFilesList from '$lib/file/BranchFilesList.svelte';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
@ -25,7 +25,7 @@
|
||||
BaseBranch,
|
||||
type CommitStatus
|
||||
} from '$lib/vbranches/types';
|
||||
import { createEventDispatcher, type Snippet } from 'svelte';
|
||||
import { type Snippet } from 'svelte';
|
||||
|
||||
export let branch: Branch | undefined = undefined;
|
||||
export let commit: Commit | RemoteCommit;
|
||||
@ -46,8 +46,7 @@
|
||||
|
||||
const currentCommitMessage = persistedCommitMessage(project.id, branch?.id || '');
|
||||
|
||||
const dispatch = createEventDispatcher<{ toggle: void }>();
|
||||
|
||||
let draggableCommitElement: HTMLElement | null = null;
|
||||
let files: RemoteFile[] = [];
|
||||
let showDetails = false;
|
||||
|
||||
@ -57,7 +56,6 @@
|
||||
|
||||
function toggleFiles() {
|
||||
showDetails = !showDetails;
|
||||
dispatch('toggle');
|
||||
|
||||
if (showDetails) loadFiles();
|
||||
}
|
||||
@ -102,6 +100,14 @@
|
||||
commitMessageModal.close();
|
||||
}
|
||||
|
||||
function getTimeAndAuthor() {
|
||||
const timeAgo = getTimeAgo(commit.createdAt);
|
||||
const author = type === 'localAndRemote' || type === 'remote' ? commit.author.name : 'you';
|
||||
return `${timeAgo} by ${author}`;
|
||||
}
|
||||
|
||||
const commitShortSha = commit.id.substring(0, 7);
|
||||
|
||||
let topHeightPx = 24;
|
||||
|
||||
$: {
|
||||
@ -109,6 +115,9 @@
|
||||
if (first) topHeightPx = 58;
|
||||
if (showDetails && !first) topHeightPx += 12;
|
||||
}
|
||||
|
||||
let dragDirection: 'up' | 'down' | undefined;
|
||||
let isDragTargeted = false;
|
||||
</script>
|
||||
|
||||
<Modal bind:this={commitMessageModal} width="small">
|
||||
@ -138,40 +147,77 @@
|
||||
class:is-last={last}
|
||||
class:has-lines={lines}
|
||||
>
|
||||
{#if dragDirection && isDragTargeted}
|
||||
<div
|
||||
class="pseudo-reorder-zone"
|
||||
class:top={dragDirection === 'up'}
|
||||
class:bottom={dragDirection === 'down'}
|
||||
class:is-first={first}
|
||||
class:is-last={last}
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
{#if lines}
|
||||
<div>
|
||||
{@render lines(topHeightPx)}
|
||||
</div>
|
||||
{/if}
|
||||
<CommitDragItem {commit}>
|
||||
<div class="commit-card" class:is-first={first} class:is-last={last}>
|
||||
<div
|
||||
class="accent-border-line"
|
||||
class:is-first={first}
|
||||
class:is-last={last}
|
||||
class:local={type === 'local'}
|
||||
class:local-and-remote={type === 'localAndRemote'}
|
||||
class:upstream={type === 'remote'}
|
||||
class:integrated={type === 'integrated'}
|
||||
></div>
|
||||
|
||||
<div class="commit-card" class:is-first={first} class:is-last={last}>
|
||||
<CommitDragItem {commit}>
|
||||
<!-- GENERAL INFO -->
|
||||
<div
|
||||
bind:this={draggableCommitElement}
|
||||
class="commit__header"
|
||||
on:click={toggleFiles}
|
||||
on:keyup={onKeyup}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
use:draggable={commit instanceof Commit
|
||||
on:dragenter={() => {
|
||||
isDragTargeted = true;
|
||||
}}
|
||||
on:dragleave={() => {
|
||||
isDragTargeted = false;
|
||||
}}
|
||||
on:drop={() => {
|
||||
isDragTargeted = false;
|
||||
}}
|
||||
on:drag={(e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const targetHeight = target.offsetHeight;
|
||||
const targetTop = target.getBoundingClientRect().top;
|
||||
const mouseY = e.clientY;
|
||||
|
||||
const isTop = mouseY < targetTop + targetHeight / 2;
|
||||
|
||||
dragDirection = isTop ? 'up' : 'down';
|
||||
}}
|
||||
use:draggableCommit={commit instanceof Commit && !isUnapplied && type !== 'integrated'
|
||||
? {
|
||||
label: commit.descriptionTitle,
|
||||
sha: commitShortSha,
|
||||
dateAndAuthor: getTimeAndAuthor(),
|
||||
commitType: type,
|
||||
data: new DraggableCommit(commit.branchId, commit, isHeadCommit),
|
||||
extendWithClass: 'commit_draggable'
|
||||
viewportId: 'board-viewport'
|
||||
}
|
||||
: nonDraggable()}
|
||||
>
|
||||
<div class="commit__drag-icon">
|
||||
<Icon name="draggable-narrow" />
|
||||
</div>
|
||||
<div
|
||||
class="accent-border-line"
|
||||
class:is-first={first}
|
||||
class:is-last={last}
|
||||
class:local={type === 'local'}
|
||||
class:local-and-remote={type === 'localAndRemote'}
|
||||
class:upstream={type === 'remote'}
|
||||
class:integrated={type === 'integrated'}
|
||||
></div>
|
||||
|
||||
{#if type === 'local' || type === 'localAndRemote'}
|
||||
<div class="commit__drag-icon">
|
||||
<Icon name="draggable-narrow" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if first}
|
||||
<div class="commit__type text-semibold text-base-12">
|
||||
@ -209,7 +255,7 @@
|
||||
class="commit__subtitle-btn commit__subtitle-btn_dashed"
|
||||
on:click|stopPropagation={() => copyToClipboard(commit.id)}
|
||||
>
|
||||
<span>{commit.id.substring(0, 7)}</span>
|
||||
<span>{commitShortSha}</span>
|
||||
|
||||
<div class="commit__subtitle-btn__icon">
|
||||
<Icon name="copy-small" />
|
||||
@ -235,11 +281,7 @@
|
||||
|
||||
<span class="commit__subtitle-divider">•</span>
|
||||
|
||||
<span
|
||||
>{getTimeAgo(commit.createdAt)}{type === 'localAndRemote' || type === 'remote'
|
||||
? ` by ${commit.author.name}`
|
||||
: ' by you'}</span
|
||||
>
|
||||
<span>{getTimeAndAuthor()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@ -285,25 +327,11 @@
|
||||
<BranchFilesList {files} {isUnapplied} readonly={type === 'remote'} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CommitDragItem>
|
||||
</CommitDragItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
/* amend drop zone */
|
||||
:global(.amend-dz-active .amend-dz-marker) {
|
||||
display: flex;
|
||||
}
|
||||
:global(.amend-dz-hover .hover-text) {
|
||||
visibility: visible;
|
||||
}
|
||||
:global(.commit_draggable) {
|
||||
cursor: grab;
|
||||
background-color: var(--clr-bg-1);
|
||||
border-radius: var(--radius-m);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.commit-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@ -321,6 +349,7 @@
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
background-color: var(--clr-bg-1);
|
||||
border-right: 1px solid var(--clr-border-2);
|
||||
@ -345,8 +374,12 @@
|
||||
|
||||
.accent-border-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
z-index: var(--z-ground);
|
||||
|
||||
&.local {
|
||||
background-color: var(--clr-commit-local);
|
||||
}
|
||||
@ -517,4 +550,29 @@
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* PSUEDO DROPZONE */
|
||||
.pseudo-reorder-zone {
|
||||
z-index: var(--z-lifted);
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background-color: var(--clr-theme-pop-element);
|
||||
}
|
||||
|
||||
.pseudo-reorder-zone.top {
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.pseudo-reorder-zone.bottom {
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
.pseudo-reorder-zone.top.is-first {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.pseudo-reorder-zone.bottom.is-last {
|
||||
bottom: -6px;
|
||||
}
|
||||
</style>
|
||||
|
@ -38,6 +38,7 @@
|
||||
$commitMessage = '';
|
||||
} finally {
|
||||
isCommitting = false;
|
||||
$expanded = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -38,7 +38,14 @@
|
||||
{@render squashDropzone()}
|
||||
|
||||
{#snippet overlay({ hovered, activated })}
|
||||
<CardOverlay {hovered} {activated} label="Amend commit" />
|
||||
<CardOverlay
|
||||
{hovered}
|
||||
{activated}
|
||||
label="Amend commit"
|
||||
extraPaddings={{
|
||||
left: 4
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
</Dropzone>
|
||||
{/snippet}
|
||||
@ -48,7 +55,14 @@
|
||||
{@render children()}
|
||||
|
||||
{#snippet overlay({ hovered, activated })}
|
||||
<CardOverlay {hovered} {activated} label="Squash commit" />
|
||||
<CardOverlay
|
||||
{hovered}
|
||||
{activated}
|
||||
label="Squash commit"
|
||||
extraPaddings={{
|
||||
left: 4
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
</Dropzone>
|
||||
{/snippet}
|
||||
@ -57,6 +71,5 @@
|
||||
.dropzone-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
@ -149,8 +149,8 @@
|
||||
{@render reorderDropzone(
|
||||
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
|
||||
getReorderDropzoneOffset({
|
||||
isLast: $localAndRemoteCommits.length === 0 && idx + 1 === $localCommits.length,
|
||||
isMiddle: $localAndRemoteCommits.length > 0 && idx + 1 === $localCommits.length
|
||||
isLast: idx + 1 === $localCommits.length,
|
||||
isMiddle: idx + 1 === $localCommits.length
|
||||
})
|
||||
)}
|
||||
|
||||
@ -181,7 +181,8 @@
|
||||
{@render reorderDropzone(
|
||||
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
|
||||
getReorderDropzoneOffset({
|
||||
isLast: idx + 1 === $localAndRemoteCommits.length
|
||||
isMiddle: idx + 1 === $localAndRemoteCommits.length
|
||||
// isLast: idx + 1 === $localAndRemoteCommits.length
|
||||
})
|
||||
)}
|
||||
<InsertEmptyCommitAction
|
||||
@ -240,6 +241,7 @@
|
||||
|
||||
<style lang="postcss">
|
||||
.commits {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--clr-bg-2);
|
||||
|
@ -4,7 +4,7 @@
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import BranchDropzone from '$lib/branch/BranchDropzone.svelte';
|
||||
import BranchLane from '$lib/branch/BranchLane.svelte';
|
||||
import { cloneWithRotation } from '$lib/dragging/draggable';
|
||||
import { cloneElement } from '$lib/dragging/draggable';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import Icon from '$lib/shared/Icon.svelte';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
@ -94,12 +94,12 @@
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
clone = cloneWithRotation(e.target);
|
||||
clone = cloneElement(e.target as HTMLElement);
|
||||
document.body.appendChild(clone);
|
||||
// Get chromium to fire dragover & drop events
|
||||
// https://stackoverflow.com/questions/6481094/html5-drag-and-drop-ondragover-not-firing-in-chrome/6483205#6483205
|
||||
e.dataTransfer?.setData('text/html', 'd'); // cannot be empty string
|
||||
e.dataTransfer?.setDragImage(clone, e.offsetX + 30, e.offsetY + 30); // Adds the padding
|
||||
e.dataTransfer?.setDragImage(clone, e.offsetX, e.offsetY); // Adds the padding
|
||||
dragged = e.currentTarget;
|
||||
priorPosition = Array.from(dropZone.children).indexOf(dragged);
|
||||
}}
|
||||
|
@ -1,82 +1,47 @@
|
||||
import { dropzoneRegistry } from './dropzone';
|
||||
import { getVSIFileIcon } from '$lib/ext-icons';
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
import { type CommitStatus } from '$lib/vbranches/types';
|
||||
import type { Draggable } from './draggables';
|
||||
|
||||
export interface DraggableConfig {
|
||||
readonly selector?: string;
|
||||
readonly disabled?: boolean;
|
||||
readonly label?: string;
|
||||
readonly filePath?: string;
|
||||
readonly sha?: string;
|
||||
readonly dateAndAuthor?: string;
|
||||
readonly commitType?: CommitStatus;
|
||||
readonly data?: Draggable | Promise<Draggable>;
|
||||
readonly viewportId?: string;
|
||||
readonly extendWithClass?: string;
|
||||
}
|
||||
|
||||
export function applyContainerStyle(element: HTMLElement) {
|
||||
element.style.position = 'absolute';
|
||||
element.style.top = '-9999px'; // Element has to be in the DOM so we move it out of sight
|
||||
element.style.display = 'inline-block';
|
||||
element.style.padding = '30px'; // To prevent clipping of rotated element
|
||||
function createElement(
|
||||
tag: string,
|
||||
classNames: string[],
|
||||
textContent?: string,
|
||||
src?: string
|
||||
): HTMLElement {
|
||||
const el = document.createElement(tag);
|
||||
el.classList.add(...classNames);
|
||||
if (textContent) el.textContent = textContent;
|
||||
if (src) (el as HTMLImageElement).src = src;
|
||||
return el;
|
||||
}
|
||||
|
||||
export function createContainerForMultiDrag(
|
||||
children: Element[],
|
||||
extendWithClass: string | undefined
|
||||
): HTMLDivElement {
|
||||
const inner = document.createElement('div');
|
||||
inner.style.display = 'flex';
|
||||
inner.style.flexDirection = 'column';
|
||||
inner.style.gap = '0.125rem';
|
||||
|
||||
children.forEach((child) => {
|
||||
inner.appendChild(cloneWithPreservedDimensions(child, extendWithClass));
|
||||
});
|
||||
rotateElement(inner);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.appendChild(inner);
|
||||
applyContainerStyle(container);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
export function cloneWithPreservedDimensions(node: any, extendWithClass: string | undefined) {
|
||||
const clone = node.cloneNode(true) as HTMLElement;
|
||||
clone.style.height = node.clientHeight + 'px';
|
||||
clone.style.width = node.clientWidth + 'px';
|
||||
clone.classList.remove('selected-draggable');
|
||||
|
||||
extendWithClass && clone.classList.add(extendWithClass);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
export function cloneWithRotation(node: any, extendWithClass: string | undefined = undefined) {
|
||||
const container = document.createElement('div');
|
||||
const clone = cloneWithPreservedDimensions(node, extendWithClass) as HTMLElement;
|
||||
container.appendChild(clone);
|
||||
|
||||
// exclude all ignored elements from the clone
|
||||
const ignoredElements = container.querySelectorAll('[data-remove-from-draggable]');
|
||||
ignoredElements.forEach((element) => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
applyContainerStyle(container);
|
||||
|
||||
// Style the inner node so it retains the shape and then rotate
|
||||
// TODO: This rotation puts a requirement on draggables to have
|
||||
// an outer container, which feels extra. Consider refactoring.
|
||||
rotateElement(clone);
|
||||
return container as HTMLElement;
|
||||
}
|
||||
|
||||
function rotateElement(element: HTMLElement) {
|
||||
element.style.rotate = `${Math.floor(Math.random() * 3)}deg`;
|
||||
}
|
||||
|
||||
export function draggable(node: HTMLElement, initialOpts: DraggableConfig) {
|
||||
let opts = initialOpts;
|
||||
function setupDragHandlers(
|
||||
node: HTMLElement,
|
||||
opts: DraggableConfig,
|
||||
createClone: (opts: DraggableConfig, selectedElements: HTMLElement[]) => HTMLElement,
|
||||
params: {
|
||||
handlerWidth: boolean;
|
||||
maxHeight?: number;
|
||||
} = {
|
||||
handlerWidth: false
|
||||
}
|
||||
) {
|
||||
let dragHandle: HTMLElement | null;
|
||||
let clone: HTMLElement | undefined;
|
||||
|
||||
let clone: HTMLElement;
|
||||
let selectedElements: HTMLElement[] = [];
|
||||
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
@ -84,84 +49,75 @@ export function draggable(node: HTMLElement, initialOpts: DraggableConfig) {
|
||||
}
|
||||
|
||||
function handleDragStart(e: DragEvent) {
|
||||
let elt: HTMLElement | null = dragHandle;
|
||||
e.stopPropagation();
|
||||
|
||||
while (elt) {
|
||||
if (elt.dataset.noDrag !== undefined) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
elt = elt.parentElement;
|
||||
if (dragHandle && dragHandle.dataset.noDrag !== undefined) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the draggable specifies a selector then we check if we're dragging selected elements
|
||||
if (opts.selector) {
|
||||
// Checking for selected siblings in the parent of the parent container likely works
|
||||
// for most use-cases but it was done here primarily for dragging multiple files.
|
||||
const parentNode = node.parentNode?.parentNode;
|
||||
selectedElements = parentNode
|
||||
? Array.from(parentNode.querySelectorAll(opts.selector).values() as Iterable<HTMLElement>)
|
||||
: [];
|
||||
|
||||
if (selectedElements.length > 0) {
|
||||
clone = createContainerForMultiDrag(selectedElements, opts.extendWithClass);
|
||||
// Dim the original element while dragging
|
||||
selectedElements.forEach((element) => {
|
||||
element.style.opacity = '0.5';
|
||||
});
|
||||
const parentNode = node.parentElement?.parentElement;
|
||||
if (!parentNode) {
|
||||
console.error('draggable parent node not found');
|
||||
return;
|
||||
}
|
||||
selectedElements = Array.from(
|
||||
parentNode.querySelectorAll(opts.selector) as NodeListOf<HTMLElement>
|
||||
);
|
||||
}
|
||||
|
||||
if (!clone) {
|
||||
clone = cloneWithRotation(node, opts.extendWithClass);
|
||||
if (selectedElements.length === 0) {
|
||||
selectedElements = [node];
|
||||
}
|
||||
|
||||
clone = createClone(opts, selectedElements);
|
||||
if (params.handlerWidth) {
|
||||
clone.style.width = node.clientWidth + 'px';
|
||||
}
|
||||
if (params.maxHeight) {
|
||||
clone.style.maxHeight = pxToRem(params.maxHeight);
|
||||
}
|
||||
|
||||
// console.log('selectedElements', selectedElements);
|
||||
selectedElements.forEach((el) => el.classList.add('drag-handle'));
|
||||
document.body.appendChild(clone);
|
||||
|
||||
Array.from(dropzoneRegistry.values()).forEach((dropzone) => {
|
||||
dropzone.register(opts.data);
|
||||
});
|
||||
|
||||
// Get chromium to fire dragover & drop events
|
||||
// https://stackoverflow.com/questions/6481094/html5-drag-and-drop-ondragover-not-firing-in-chrome/6483205#6483205
|
||||
e.dataTransfer?.setData('text/html', 'placeholder copy'); // cannot be empty string
|
||||
e.dataTransfer?.setDragImage(clone, e.offsetX + 30, e.offsetY + 30); // Adds the padding
|
||||
e.stopPropagation();
|
||||
if (e.dataTransfer) {
|
||||
if (params.handlerWidth) {
|
||||
e.dataTransfer.setDragImage(clone, e.offsetX, e.offsetY);
|
||||
} else {
|
||||
e.dataTransfer.setDragImage(clone, clone.offsetWidth - 20, 25);
|
||||
}
|
||||
|
||||
// Get chromium to fire dragover & drop events
|
||||
// https://stackoverflow.com/questions/6481094/html5-drag-and-drop-ondragover-not-firing-in-chrome/6483205#6483205
|
||||
e.dataTransfer?.setData('text/html', 'd'); // cannot be empty string
|
||||
e.dataTransfer.effectAllowed = 'uninitialized';
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragEnd(e: DragEvent) {
|
||||
if (clone) {
|
||||
clone.remove();
|
||||
clone = undefined;
|
||||
}
|
||||
|
||||
// reset the opacity of the selected elements
|
||||
selectedElements.forEach((element) => {
|
||||
element.style.opacity = '1';
|
||||
});
|
||||
|
||||
e.stopPropagation();
|
||||
if (clone) clone.remove();
|
||||
selectedElements.forEach((el) => el.classList.remove('drag-handle'));
|
||||
Array.from(dropzoneRegistry.values()).forEach((dropzone) => {
|
||||
dropzone.unregister();
|
||||
});
|
||||
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const viewport = opts.viewportId ? document.getElementById(opts.viewportId) : null;
|
||||
const triggerRange = 150;
|
||||
const scrollSpeed = (viewport?.clientWidth || 500) / 2;
|
||||
let lastDrag = new Date().getTime();
|
||||
|
||||
function handleDrag(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
const viewport = opts.viewportId ? document.getElementById(opts.viewportId) : null;
|
||||
if (!viewport) return;
|
||||
if (new Date().getTime() - lastDrag < 500) return;
|
||||
lastDrag = new Date().getTime();
|
||||
|
||||
const triggerRange = 150;
|
||||
const scrollSpeed = (viewport.clientWidth || 500) / 2;
|
||||
const viewportWidth = viewport.clientWidth;
|
||||
const relativeX = e.clientX - viewport.getBoundingClientRect().left;
|
||||
|
||||
// Scroll horizontally if the draggable is near the edge of the viewport
|
||||
if (relativeX < triggerRange) {
|
||||
viewport.scrollBy(-scrollSpeed, 0);
|
||||
} else if (relativeX > viewportWidth - triggerRange) {
|
||||
@ -189,12 +145,123 @@ export function draggable(node: HTMLElement, initialOpts: DraggableConfig) {
|
||||
setup(opts);
|
||||
|
||||
return {
|
||||
update(opts: DraggableConfig) {
|
||||
update(newOpts: DraggableConfig) {
|
||||
clean();
|
||||
setup(opts);
|
||||
setup(newOpts);
|
||||
},
|
||||
destroy() {
|
||||
clean();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
//// COMMIT DRAGGABLE ////
|
||||
//////////////////////////
|
||||
|
||||
export function createCommitElement(
|
||||
commitType: CommitStatus | undefined,
|
||||
label: string | undefined,
|
||||
sha: string | undefined,
|
||||
dateAndAuthor: string | undefined
|
||||
): HTMLDivElement {
|
||||
const cardEl = createElement('div', ['draggable-commit']) as HTMLDivElement;
|
||||
const labelEl = createElement('span', ['text-base-13', 'text-bold'], label || 'Empty commit');
|
||||
const infoEl = createElement('div', ['draggable-commit-info', 'text-base-11']);
|
||||
const shaEl = createElement('span', ['draggable-commit-info-text'], sha);
|
||||
const dateAndAuthorEl = createElement('span', ['draggable-commit-info-text'], dateAndAuthor);
|
||||
|
||||
if (commitType) {
|
||||
const indicatorClass = `draggable-commit-${commitType}`;
|
||||
labelEl.classList.add('draggable-commit-indicator', indicatorClass);
|
||||
}
|
||||
|
||||
cardEl.appendChild(labelEl);
|
||||
infoEl.appendChild(shaEl);
|
||||
infoEl.appendChild(dateAndAuthorEl);
|
||||
cardEl.appendChild(infoEl);
|
||||
return cardEl;
|
||||
}
|
||||
|
||||
export function draggableCommit(node: HTMLElement, initialOpts: DraggableConfig) {
|
||||
function createClone(opts: DraggableConfig) {
|
||||
return createCommitElement(opts.commitType, opts.label, opts.sha, opts.dateAndAuthor);
|
||||
}
|
||||
return setupDragHandlers(node, initialOpts, createClone, {
|
||||
handlerWidth: true
|
||||
});
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
//// FILE DRAGGABLE ////
|
||||
////////////////////////
|
||||
|
||||
export function createChipsElement(
|
||||
childrenAmount: number,
|
||||
label: string | undefined,
|
||||
filePath: string | undefined
|
||||
): HTMLDivElement {
|
||||
const containerEl = createElement('div', ['draggable-chip-container']) as HTMLDivElement;
|
||||
const chipEl = createElement('div', ['draggable-chip']);
|
||||
containerEl.appendChild(chipEl);
|
||||
|
||||
if (filePath) {
|
||||
const iconEl = createElement(
|
||||
'img',
|
||||
['draggable-chip-icon'],
|
||||
undefined,
|
||||
getVSIFileIcon(filePath)
|
||||
);
|
||||
chipEl.appendChild(iconEl);
|
||||
}
|
||||
|
||||
const labelEl = createElement('span', ['text-base-12'], label);
|
||||
chipEl.appendChild(labelEl);
|
||||
|
||||
if (childrenAmount > 1) {
|
||||
const amountTag = createElement(
|
||||
'div',
|
||||
['text-base-11', 'text-bold', 'draggable-chip-amount'],
|
||||
childrenAmount.toString()
|
||||
);
|
||||
chipEl.appendChild(amountTag);
|
||||
}
|
||||
|
||||
if (childrenAmount === 2) {
|
||||
containerEl.classList.add('draggable-chip-two');
|
||||
} else if (childrenAmount > 2) {
|
||||
containerEl.classList.add('draggable-chip-multiple');
|
||||
}
|
||||
|
||||
return containerEl;
|
||||
}
|
||||
|
||||
export function draggableChips(node: HTMLElement, initialOpts: DraggableConfig) {
|
||||
function createClone(opts: DraggableConfig, selectedElements: HTMLElement[]) {
|
||||
return createChipsElement(selectedElements.length, opts.label, opts.filePath);
|
||||
}
|
||||
return setupDragHandlers(node, initialOpts, createClone);
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
//// HUNK DRAGGABLE ////
|
||||
////////////////////////
|
||||
|
||||
export function cloneElement(node: HTMLElement) {
|
||||
const cloneEl = node.cloneNode(true) as HTMLElement;
|
||||
|
||||
// exclude all ignored elements from the clone
|
||||
const ignoredElements = Array.from(cloneEl.querySelectorAll('[data-remove-from-draggable]'));
|
||||
ignoredElements.forEach((el) => el.remove());
|
||||
|
||||
return cloneEl;
|
||||
}
|
||||
|
||||
export function draggableElement(node: HTMLElement, initialOpts: DraggableConfig) {
|
||||
function createClone() {
|
||||
return cloneElement(node);
|
||||
}
|
||||
return setupDragHandlers(node, initialOpts, createClone, {
|
||||
handlerWidth: true
|
||||
});
|
||||
}
|
||||
|
@ -1,48 +1,83 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/shared/Icon.svelte';
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
import { scale } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
hovered: boolean;
|
||||
activated: boolean;
|
||||
label?: string;
|
||||
extraPaddings?: {
|
||||
top?: number;
|
||||
right?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
};
|
||||
}
|
||||
|
||||
const { hovered, activated, label = 'Drop here' }: Props = $props();
|
||||
const { hovered, activated, label = 'Drop here', extraPaddings }: Props = $props();
|
||||
let defaultPadding = $derived.by(() => {
|
||||
if (hovered) return 2;
|
||||
return 4;
|
||||
});
|
||||
|
||||
const extraPaddingTop = extraPaddings?.top ?? 0;
|
||||
const extraPaddingRight = extraPaddings?.right ?? 0;
|
||||
const extraPaddingBottom = extraPaddings?.bottom ?? 0;
|
||||
const extraPaddingLeft = extraPaddings?.left ?? 0;
|
||||
</script>
|
||||
|
||||
<div class="dropzone-target container" class:activated class:hovered>
|
||||
<div class="dropzone-content">
|
||||
<Icon name="new-file-small-filled" />
|
||||
<p class="text-base-13">{label}</p>
|
||||
<div
|
||||
transition:scale={{ duration: 200, start: 0.9 }}
|
||||
class="dropzone-target dropzone-wrapper"
|
||||
class:activated
|
||||
class:hovered
|
||||
style="--padding-top: {pxToRem(defaultPadding + extraPaddingTop)}; --padding-right: {pxToRem(
|
||||
defaultPadding + extraPaddingRight
|
||||
)}; --padding-bottom: {pxToRem(defaultPadding + extraPaddingBottom)}; --padding-left: {pxToRem(
|
||||
defaultPadding + extraPaddingLeft
|
||||
)}"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="dropzone-label">
|
||||
<svg
|
||||
class="dropzone-label-icon"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M11 7L6 2M6 2L1 7M6 2L6 12" stroke="white" stroke-width="1.5" />
|
||||
</svg>
|
||||
|
||||
<span class="text-base-12 text-semibold">{label}</span>
|
||||
</div>
|
||||
|
||||
<!-- add svg rectange -->
|
||||
<svg width="100%" height="100%" class="animated-rectangle">
|
||||
<rect width="100%" height="100%" rx="5" ry="5" vector-effect="non-scaling-stroke" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
:root {
|
||||
--dropzone-height: 16px;
|
||||
--dropzone-overlap: calc(var(--dropzone-height) / 2);
|
||||
}
|
||||
|
||||
.container {
|
||||
.dropzone-wrapper {
|
||||
z-index: var(--z-ground);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding-top: var(--padding-top);
|
||||
padding-right: var(--padding-right);
|
||||
padding-bottom: var(--padding-bottom);
|
||||
padding-left: var(--padding-left);
|
||||
|
||||
display: flex;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background-color: oklch(from var(--clr-scale-pop-70) l c h / 0.1);
|
||||
|
||||
outline-color: var(--clr-scale-pop-40);
|
||||
outline-style: dashed;
|
||||
outline-width: 1px;
|
||||
outline-offset: -10px;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
z-index: var(--z-lifted);
|
||||
|
||||
transition: background-color 0.1s;
|
||||
transition:
|
||||
transform 0.1s,
|
||||
padding 0.1s;
|
||||
|
||||
/* It is very important that all children are pointer-events: none */
|
||||
/* https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element */
|
||||
@ -50,19 +85,98 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:not(.activated) {
|
||||
display: none;
|
||||
&.activated {
|
||||
display: flex;
|
||||
animation: dropzone-scale 0.1s forwards;
|
||||
}
|
||||
|
||||
&.hovered {
|
||||
background-color: oklch(from var(--clr-scale-pop-20) l c h / 0.1);
|
||||
transform: scale(1.01);
|
||||
|
||||
.animated-rectangle rect {
|
||||
fill: oklch(from var(--clr-scale-pop-50) l c h / 0.14);
|
||||
}
|
||||
|
||||
.dropzone-label {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropzone-content {
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dropzone-label {
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--clr-scale-pop-40);
|
||||
padding: 6px 10px;
|
||||
border-radius: 100px;
|
||||
color: var(--clr-theme-pop-on-element);
|
||||
background-color: var(--clr-theme-pop-element);
|
||||
transform: translateY(3px) scale(0.95);
|
||||
|
||||
transition:
|
||||
opacity 0.1s,
|
||||
transform 0.15s;
|
||||
}
|
||||
|
||||
.dropzone-label-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
animation: icon-shifting 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes icon-shifting {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animated-rectangle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
& rect {
|
||||
fill: oklch(from var(--clr-scale-pop-50) l c h / 0.1);
|
||||
stroke: var(--clr-scale-pop-50);
|
||||
|
||||
stroke-width: 2px;
|
||||
stroke-dasharray: 2;
|
||||
stroke-dashoffset: 30;
|
||||
transform-origin: center;
|
||||
|
||||
transition:
|
||||
fill var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
|
||||
animation: dash 4s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
from {
|
||||
stroke-dashoffset: 30;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -13,21 +13,21 @@
|
||||
<div
|
||||
class="dropzone-target container"
|
||||
class:activated
|
||||
style="--y-offset: {pxToRem(yOffsetPx) || 0}"
|
||||
class:hovered
|
||||
style:--y-offset={pxToRem(yOffsetPx)}
|
||||
>
|
||||
<div class="indicator" class:hovered></div>
|
||||
<div class="indicator"></div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
:root {
|
||||
--dropzone-height: 16px;
|
||||
--dropzone-overlap: calc(var(--dropzone-height) / 2);
|
||||
}
|
||||
|
||||
.container {
|
||||
--dropzone-overlap: calc(var(--dropzone-height) / 2);
|
||||
--dropzone-height: 16px;
|
||||
|
||||
height: var(--dropzone-height);
|
||||
margin-top: calc(var(--dropzone-overlap) * -1);
|
||||
margin-bottom: calc(var(--dropzone-overlap) * -1);
|
||||
/* background-color: rgba(0, 0, 0, 0.1); */
|
||||
width: 100%;
|
||||
position: relative;
|
||||
top: var(--y-offset);
|
||||
@ -35,7 +35,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
z-index: 101;
|
||||
z-index: var(--z-floating);
|
||||
|
||||
/* It is very important that all children are pointer-events: none */
|
||||
/* https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element */
|
||||
@ -46,17 +46,19 @@
|
||||
&:not(.activated) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.hovered {
|
||||
& .indicator {
|
||||
background-color: var(--clr-theme-pop-element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
height: 2px;
|
||||
margin-top: 1px;
|
||||
transition: opacity 0.1s;
|
||||
background-color: var(--clr-border-2);
|
||||
opacity: 0;
|
||||
|
||||
&.hovered {
|
||||
opacity: 1;
|
||||
}
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
@ -2,6 +2,7 @@
|
||||
import FileStatusTag from './FileStatusTag.svelte';
|
||||
import { getVSIFileIcon } from '$lib/ext-icons';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import { splitFilePath } from '$lib/utils/filePath';
|
||||
import { computeFileStatus } from '$lib/utils/fileStatus';
|
||||
import { computeAddedRemovedByFiles } from '$lib/utils/metrics';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
@ -14,22 +15,12 @@
|
||||
$: fileStats = computeAddedRemovedByFiles(file);
|
||||
$: fileStatus = computeFileStatus(file);
|
||||
|
||||
function boldenFilename(filepath: string): { filename: string; path: string } {
|
||||
const parts = filepath.split('/');
|
||||
if (parts.length === 0) return { filename: '', path: '' };
|
||||
|
||||
const filename = parts[parts.length - 1];
|
||||
const path = parts.slice(0, -1).join('/');
|
||||
|
||||
return { filename, path };
|
||||
}
|
||||
|
||||
$: fileTitle = boldenFilename(file.path);
|
||||
$: fileTitle = splitFilePath(file.path);
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<div class="header__inner">
|
||||
<img src={getVSIFileIcon(file.path)} alt="" width="13" height="13" class="icon" />
|
||||
<img src={getVSIFileIcon(file.path)} alt="" class="icon" />
|
||||
<div class="header__info truncate">
|
||||
<div class="header__filetitle text-base-13 truncate">
|
||||
<span class="header__filename">{fileTitle.filename}</span>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FileContextMenu from './FileContextMenu.svelte';
|
||||
import FileStatusIcons from './FileStatusIcons.svelte';
|
||||
import { draggable } from '$lib/dragging/draggable';
|
||||
import { draggableChips } from '$lib/dragging/draggable';
|
||||
import { DraggableFile } from '$lib/dragging/draggables';
|
||||
import { getVSIFileIcon } from '$lib/ext-icons';
|
||||
import Checkbox from '$lib/shared/Checkbox.svelte';
|
||||
@ -116,7 +116,9 @@
|
||||
popupMenu.openByMouse(e, { files: [file] });
|
||||
}
|
||||
}}
|
||||
use:draggable={{
|
||||
use:draggableChips={{
|
||||
label: `${file.filename}`,
|
||||
filePath: file.path,
|
||||
data: $selectedFiles.then(
|
||||
(files) => new DraggableFile($branch?.id || '', file, $commit, files)
|
||||
),
|
||||
@ -227,6 +229,7 @@
|
||||
}
|
||||
|
||||
.info {
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
|
@ -4,6 +4,7 @@
|
||||
import { getVSIFileIcon } from '$lib/ext-icons';
|
||||
import { createdOnDay } from '$lib/history/history';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import { splitFilePath } from '$lib/utils/filePath';
|
||||
import { toHumanReadableTime } from '$lib/utils/time';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { Snapshot, SnapshotDetails } from '$lib/history/types';
|
||||
@ -152,10 +153,6 @@
|
||||
const error = entry.details?.trailers.find((t) => t.key === 'error')?.value;
|
||||
|
||||
const operation = mapOperation(entry.details);
|
||||
|
||||
function getPathOnly(path: string) {
|
||||
return path.split('/').slice(0, -1).join('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@ -225,10 +222,10 @@
|
||||
/>
|
||||
<div class="text-base-12 files-attacment__file-path-and-name">
|
||||
<span class="files-attacment__file-name">
|
||||
{filePath.split('/').pop()}
|
||||
{splitFilePath(filePath).filename}
|
||||
</span>
|
||||
<span class="files-attacment__file-path">
|
||||
{getPathOnly(filePath)}
|
||||
{splitFilePath(filePath).path}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { draggable } from '$lib/dragging/draggable';
|
||||
import { draggableElement } from '$lib/dragging/draggable';
|
||||
import { DraggableHunk } from '$lib/dragging/draggables';
|
||||
import HunkContextMenu from '$lib/hunk/HunkContextMenu.svelte';
|
||||
import HunkLines from '$lib/hunk/HunkLines.svelte';
|
||||
@ -63,7 +63,7 @@
|
||||
bind:this={viewport}
|
||||
tabindex="0"
|
||||
role="cell"
|
||||
use:draggable={{
|
||||
use:draggableElement={{
|
||||
data: new DraggableHunk($branch?.id || '', section.hunk),
|
||||
disabled: draggingDisabled
|
||||
}}
|
||||
|
@ -97,6 +97,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
data-remove-from-draggable
|
||||
on:mousedown={onMouseDown}
|
||||
on:click|stopPropagation
|
||||
on:dblclick|stopPropagation
|
||||
|
@ -57,7 +57,13 @@
|
||||
{padding}
|
||||
{shift}
|
||||
{thickness}
|
||||
on:dragging={(e) => dispatch('dragging', e.detail)}
|
||||
on:dragging={(data) => dispatch('dragging', data.detail)}
|
||||
on:scroll={(data) => {
|
||||
const event = data.detail;
|
||||
const target = event.target as HTMLDivElement;
|
||||
|
||||
scrolled = target.scrollTop > 0;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -75,7 +81,9 @@
|
||||
width: 100%;
|
||||
}
|
||||
.contents {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
.scrolled {
|
||||
|
@ -61,6 +61,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
dragging: boolean;
|
||||
scroll: Event;
|
||||
}>();
|
||||
|
||||
/////////////////////
|
||||
@ -190,9 +191,11 @@
|
||||
};
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
function onScroll(e: Event) {
|
||||
if (!isScrollable) return;
|
||||
|
||||
dispatch('scroll', e);
|
||||
|
||||
clearTimer();
|
||||
setupTimer();
|
||||
|
||||
@ -276,6 +279,7 @@
|
||||
|
||||
<div
|
||||
bind:this={track}
|
||||
data-remove-from-draggable
|
||||
class="scrollbar-track"
|
||||
class:horz
|
||||
class:vert
|
||||
|
9
app/src/lib/utils/filePath.ts
Normal file
9
app/src/lib/utils/filePath.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function splitFilePath(filepath: string): { filename: string; path: string } {
|
||||
const parts = filepath.split('/');
|
||||
if (parts.length === 0) return { filename: '', path: '' };
|
||||
|
||||
const filename = parts[parts.length - 1];
|
||||
const path = parts.slice(0, -1).join('/');
|
||||
|
||||
return { filename, path };
|
||||
}
|
162
app/src/styles/draggable.css
Normal file
162
app/src/styles/draggable.css
Normal file
@ -0,0 +1,162 @@
|
||||
.draggable-chip-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.draggable-chip {
|
||||
z-index: 3;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background-color: var(--clr-bg-1);
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--clr-border-2);
|
||||
padding: 8px;
|
||||
min-width: 50px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.draggable-chip-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.draggable-chip-amount {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--clr-scale-ntrl-20);
|
||||
color: var(--clr-scale-ntrl-100);
|
||||
padding: 2px 4px;
|
||||
min-width: 16px;
|
||||
border-radius: 16px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/* if dragging more then one item */
|
||||
.draggable-chip-two {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--clr-border-2);
|
||||
background-color: var(--clr-bg-1);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
}
|
||||
|
||||
.draggable-chip-multiple {
|
||||
&::after,
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--clr-border-2);
|
||||
background-color: var(--clr-bg-1);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
|
||||
&::before {
|
||||
z-index: 2;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
z-index: 1;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* COMMIT */
|
||||
.draggable-commit {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
background-color: var(--clr-bg-1);
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--clr-border-2);
|
||||
padding: 12px 12px 12px 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.draggable-commit-indicator {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.draggable-commit-localAndRemote {
|
||||
&::before {
|
||||
background-color: var(--clr-commit-remote);
|
||||
}
|
||||
}
|
||||
|
||||
.draggable-commit-local {
|
||||
color: var(--clr-text-1);
|
||||
&::before {
|
||||
background-color: var(--clr-commit-local);
|
||||
}
|
||||
}
|
||||
|
||||
.draggable-commit-integrated {
|
||||
color: var(--clr-text-1);
|
||||
&::before {
|
||||
background-color: var(--clr-commit-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.draggable-commit-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* gap: 8px; */
|
||||
}
|
||||
|
||||
.draggable-commit-info-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--clr-text-2);
|
||||
|
||||
&:not(:last-child):after {
|
||||
content: '•';
|
||||
margin: 0 5px;
|
||||
color: var(--clr-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
opacity: 0.5;
|
||||
|
||||
/* disable children events in order to prevent false dragleave events */
|
||||
& > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropzone-scale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@
|
||||
@import './text-input.css';
|
||||
@import './commit-lines.css';
|
||||
@import './markdown.css';
|
||||
@import './draggable.css';
|
||||
|
||||
/* CSS VARIABLES */
|
||||
:root {
|
||||
|
Loading…
Reference in New Issue
Block a user