mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 20:54:50 +03:00
Branch name input improvments (#3507)
* Resizer hooks improved - Resizer hook updated in order to get an accurate value without post adjustment - Naming inconsistency fixed in `useResizer` * Refactor: name input - return initial name if a user trying to submit an empty branch name - removed extra elements and CSS - Input handling with less code * trim lane name
This commit is contained in:
parent
7908d0a195
commit
6c25a7d5bc
@ -31,6 +31,8 @@
|
||||
let branchName = branch?.upstreamName || normalizeBranchName($branchStore.name);
|
||||
|
||||
function handleBranchNameChange(title: string) {
|
||||
if (title == '') return;
|
||||
|
||||
branchName = normalizeBranchName(title);
|
||||
branchController.updateBranchName(branch.id, title);
|
||||
}
|
||||
@ -87,40 +89,44 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="header__wrapper">
|
||||
<div class="header card" class:isUnapplied>
|
||||
<div class="header__info">
|
||||
<div class="header__label">
|
||||
<div class="header card">
|
||||
<div class="header__info-wrapper">
|
||||
{#if !isUnapplied}
|
||||
<div class="draggable" data-drag-handle>
|
||||
<Icon name="draggable" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="header__info">
|
||||
<BranchLabel
|
||||
name={branch.name || 'hello'}
|
||||
name={branch.name}
|
||||
on:change={(e) => handleBranchNameChange(e.detail.name)}
|
||||
disabled={isUnapplied}
|
||||
/>
|
||||
</div>
|
||||
<div class="header__remote-branch">
|
||||
<ActiveBranchStatus
|
||||
branchName={branch.upstreamName ?? branchName}
|
||||
{isUnapplied}
|
||||
{hasIntegratedCommits}
|
||||
remoteExists={!!branch.upstreamName}
|
||||
isLaneCollapsed={$isLaneCollapsed}
|
||||
/>
|
||||
<div class="header__remote-branch">
|
||||
<ActiveBranchStatus
|
||||
branchName={branch.upstreamName ?? branchName}
|
||||
{isUnapplied}
|
||||
{hasIntegratedCommits}
|
||||
remoteExists={!!branch.upstreamName}
|
||||
isLaneCollapsed={$isLaneCollapsed}
|
||||
/>
|
||||
|
||||
{#await branch.isMergeable then isMergeable}
|
||||
{#if !isMergeable}
|
||||
<Tag
|
||||
icon="locked-small"
|
||||
style="warning"
|
||||
help="Applying this branch will add merge conflict markers that you will have to resolve"
|
||||
>
|
||||
Conflict
|
||||
</Tag>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
<div class="draggable" data-drag-handle>
|
||||
<Icon name="draggable" />
|
||||
{#await branch.isMergeable then isMergeable}
|
||||
{#if !isMergeable}
|
||||
<Tag
|
||||
icon="locked-small"
|
||||
style="warning"
|
||||
help="Applying this branch will add merge conflict markers that you will have to resolve"
|
||||
>
|
||||
Conflict
|
||||
</Tag>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header__actions">
|
||||
<div class="header__buttons">
|
||||
{#if branch.active}
|
||||
@ -229,7 +235,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
<style>
|
||||
.header__wrapper {
|
||||
z-index: var(--z-lifted);
|
||||
position: sticky;
|
||||
@ -240,15 +246,6 @@
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2);
|
||||
|
||||
&:hover {
|
||||
& .draggable {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&.isUnapplied {
|
||||
background: var(--clr-bg-alt);
|
||||
}
|
||||
}
|
||||
.header__top-overlay {
|
||||
z-index: var(--z-ground);
|
||||
@ -258,13 +255,17 @@
|
||||
width: 100%;
|
||||
height: var(--size-20);
|
||||
background: var(--target-branch-background);
|
||||
/* background-color: red; */
|
||||
}
|
||||
.header__info-wrapper {
|
||||
display: flex;
|
||||
gap: var(--size-2);
|
||||
padding: var(--size-10);
|
||||
}
|
||||
.header__info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: margin var(--transition-slow);
|
||||
padding: var(--size-12);
|
||||
overflow: hidden;
|
||||
gap: var(--size-10);
|
||||
}
|
||||
.header__actions {
|
||||
@ -276,31 +277,19 @@
|
||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||
user-select: none;
|
||||
}
|
||||
.isUnapplied .header__actions {
|
||||
border-top: 1px solid var(--clr-border-main);
|
||||
}
|
||||
|
||||
.header__buttons {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: var(--size-4);
|
||||
}
|
||||
.header__label {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
gap: var(--size-4);
|
||||
}
|
||||
.draggable {
|
||||
display: flex;
|
||||
height: fit-content;
|
||||
cursor: grab;
|
||||
position: absolute;
|
||||
right: var(--size-4);
|
||||
top: var(--size-6);
|
||||
opacity: 0;
|
||||
padding: var(--size-2) var(--size-2) 0 0;
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
transition:
|
||||
opacity var(--transition-slow),
|
||||
color var(--transition-slow);
|
||||
transition: color var(--transition-slow);
|
||||
|
||||
&:hover {
|
||||
color: var(--clr-scale-ntrl-40);
|
||||
@ -326,7 +315,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* COLLAPSABLE LANE */
|
||||
/* COLLAPSIBLE LANE */
|
||||
|
||||
.collapsed-lane {
|
||||
cursor: default;
|
||||
|
@ -1,119 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { useResize } from '$lib/utils/useResize';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let name: string;
|
||||
export let disabled = false;
|
||||
let inputActive = false;
|
||||
let label: HTMLDivElement;
|
||||
let input: HTMLInputElement;
|
||||
|
||||
function activateInput() {
|
||||
if (disabled) return;
|
||||
inputActive = true;
|
||||
setTimeout(() => input.select(), 0);
|
||||
}
|
||||
|
||||
let inputEl: HTMLInputElement;
|
||||
let initialName = name;
|
||||
|
||||
let mesureEl: HTMLSpanElement;
|
||||
let inputWidth = 0;
|
||||
let inputWidth: string | undefined;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: { name: string };
|
||||
}>();
|
||||
|
||||
$: {
|
||||
if (mesureEl) {
|
||||
inputWidth = mesureEl.getBoundingClientRect().width;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if inputActive}
|
||||
<span class="branch-name-mesure-el text-base-13 text-bold" bind:this={mesureEl}>{name}</span>
|
||||
<input
|
||||
type="text"
|
||||
{disabled}
|
||||
bind:this={input}
|
||||
bind:value={name}
|
||||
on:change={(e) => dispatch('change', { name: e.currentTarget.value })}
|
||||
on:input={() => {
|
||||
if (input.value.length > 0) {
|
||||
inputWidth = mesureEl.getBoundingClientRect().width;
|
||||
} else {
|
||||
inputWidth = 0;
|
||||
}
|
||||
}}
|
||||
title={name}
|
||||
class="branch-name-input text-base-13 text-bold"
|
||||
on:dblclick|stopPropagation
|
||||
on:blur={() => (inputActive = false)}
|
||||
on:keydown={(e) => {
|
||||
if (e.key == 'Enter') {
|
||||
// Unmount input field asynchronously to ensure on:change gets executed.
|
||||
setTimeout(() => (inputActive = false), 0);
|
||||
setTimeout(() => label.focus(), 0);
|
||||
}
|
||||
|
||||
if (e.key == 'Escape') {
|
||||
inputActive = false;
|
||||
name = initialName;
|
||||
setTimeout(() => label.focus(), 0);
|
||||
}
|
||||
}}
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
style={`width: calc(${inputWidth}px + var(--size-12))`}
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
bind:this={label}
|
||||
role="textbox"
|
||||
tabindex="0"
|
||||
class="branch-name text-base-13 text-bold truncate"
|
||||
on:keydown={(e) => e.key == 'Enter' && activateInput()}
|
||||
on:mousedown={activateInput}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
{/if}
|
||||
<span
|
||||
use:useResize={(frame) => {
|
||||
inputWidth = `${Math.round(frame.width)}px`;
|
||||
}}
|
||||
class="branch-name-mesure-el text-base-14 text-bold"
|
||||
bind:this={mesureEl}>{name}</span
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
{disabled}
|
||||
bind:this={inputEl}
|
||||
bind:value={name}
|
||||
on:change={(e) => dispatch('change', { name: e.currentTarget.value.trim() })}
|
||||
title={name}
|
||||
class="branch-name-input text-base-14 text-bold"
|
||||
on:dblclick|stopPropagation
|
||||
on:click|stopPropagation={() => {
|
||||
inputEl.focus();
|
||||
}}
|
||||
on:blur={() => {
|
||||
if (name == '') name = initialName;
|
||||
}}
|
||||
on:focus={() => {
|
||||
initialName = name;
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key == 'Enter' || e.key == 'Escape') {
|
||||
inputEl.blur();
|
||||
}
|
||||
}}
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
style:width={inputWidth}
|
||||
/>
|
||||
|
||||
<style lang="postcss">
|
||||
.branch-name,
|
||||
.branch-name-mesure-el,
|
||||
.branch-name-input {
|
||||
min-width: 2.8rem;
|
||||
height: var(--size-20);
|
||||
pointer-events: auto;
|
||||
color: var(--clr-scale-ntrl-0);
|
||||
padding: var(--size-2) var(--size-4);
|
||||
border-radius: var(--radius-s);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.branch-name {
|
||||
cursor: text;
|
||||
display: inline-block;
|
||||
transition: background-color var(--transition-fast);
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--clr-bg-muted);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
.branch-name-mesure-el {
|
||||
pointer-events: auto;
|
||||
visibility: hidden;
|
||||
border: 2px solid transparent;
|
||||
top: 30px;
|
||||
color: black;
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
white-space: pre;
|
||||
}
|
||||
.branch-name-input {
|
||||
min-width: 1rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--clr-scale-ntrl-0);
|
||||
background-color: var(--clr-bg-main);
|
||||
outline: none;
|
||||
&:focus {
|
||||
|
||||
/* not readonly */
|
||||
&:not([disabled]):hover {
|
||||
background-color: var(--clr-bg-muted);
|
||||
}
|
||||
|
||||
&:not([disabled]):focus {
|
||||
outline: none;
|
||||
background-color: var(--clr-bg-muted);
|
||||
border-color: var(--clr-border-main);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -22,8 +22,6 @@
|
||||
const branchController = getContext(BranchController);
|
||||
const project = getContext(Project);
|
||||
|
||||
let meatballButton: HTMLDivElement;
|
||||
let container: HTMLDivElement;
|
||||
let isApplying = false;
|
||||
|
||||
function updateContextMenu(copyablePrUrl: string) {
|
||||
@ -44,11 +42,9 @@
|
||||
</script>
|
||||
|
||||
<div class="header__wrapper">
|
||||
<div class="header card" bind:this={container}>
|
||||
<div class="header card">
|
||||
<div class="header__info">
|
||||
<div class="header__label">
|
||||
<BranchLabel bind:name={branch.name} />
|
||||
</div>
|
||||
<BranchLabel disabled bind:name={branch.name} />
|
||||
<div class="header__remote-branch">
|
||||
<div
|
||||
class="status-tag text-base-11 text-semibold remote"
|
||||
@ -90,8 +86,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="header__actions">
|
||||
<div class="header__buttons"></div>
|
||||
<div class="relative" bind:this={meatballButton}>
|
||||
<div class="header__buttons">
|
||||
<Button
|
||||
style="ghost"
|
||||
kind="solid"
|
||||
@ -145,15 +140,16 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: margin var(--transition-slow);
|
||||
padding: var(--size-12);
|
||||
padding: var(--size-10);
|
||||
gap: var(--size-10);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header__actions {
|
||||
display: flex;
|
||||
gap: var(--size-4);
|
||||
background: var(--clr-bg-alt);
|
||||
padding: var(--size-14);
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||
user-select: none;
|
||||
}
|
||||
@ -162,12 +158,6 @@
|
||||
position: relative;
|
||||
gap: var(--size-4);
|
||||
}
|
||||
.header__label {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
gap: var(--size-4);
|
||||
}
|
||||
|
||||
.header__remote-branch {
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
|
@ -19,7 +19,7 @@
|
||||
import { splitMessage } from '$lib/utils/commitMessage';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
import { tooltip } from '$lib/utils/tooltip';
|
||||
import { setAutoHeight } from '$lib/utils/useAutoHeight';
|
||||
import { useAutoHeight } from '$lib/utils/useAutoHeight';
|
||||
import { useResize } from '$lib/utils/useResize';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { Ownership } from '$lib/vbranches/ownership';
|
||||
@ -69,8 +69,8 @@
|
||||
}
|
||||
|
||||
function updateHeights() {
|
||||
setAutoHeight(titleTextArea);
|
||||
setAutoHeight(descriptionTextArea);
|
||||
useAutoHeight(titleTextArea);
|
||||
useAutoHeight(descriptionTextArea);
|
||||
}
|
||||
|
||||
async function commit() {
|
||||
@ -148,9 +148,9 @@
|
||||
bind:this={titleTextArea}
|
||||
use:focusTextareaOnMount
|
||||
use:useResize={() => {
|
||||
setAutoHeight(titleTextArea);
|
||||
useAutoHeight(titleTextArea);
|
||||
}}
|
||||
on:focus={(e) => setAutoHeight(e.currentTarget)}
|
||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||
on:input={(e) => {
|
||||
$commitMessage = concatMessage(e.currentTarget.value, description);
|
||||
}}
|
||||
@ -172,8 +172,8 @@
|
||||
spellcheck="false"
|
||||
rows="1"
|
||||
bind:this={descriptionTextArea}
|
||||
use:useResize={() => setAutoHeight(descriptionTextArea)}
|
||||
on:focus={(e) => setAutoHeight(e.currentTarget)}
|
||||
use:useResize={() => useAutoHeight(descriptionTextArea)}
|
||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||
on:input={(e) => {
|
||||
$commitMessage = concatMessage(title, e.currentTarget.value);
|
||||
}}
|
||||
@ -182,7 +182,7 @@
|
||||
if (e.key == 'Backspace' && value.length == 0) {
|
||||
e.preventDefault();
|
||||
titleTextArea.focus();
|
||||
setAutoHeight(e.currentTarget);
|
||||
useAutoHeight(e.currentTarget);
|
||||
} else if (e.key == 'a' && (e.metaKey || e.ctrlKey) && value.length == 0) {
|
||||
// select previous textarea on cmd+a if this textarea is empty
|
||||
e.preventDefault();
|
||||
|
@ -1,5 +1,5 @@
|
||||
export function setAutoHeight(element: HTMLTextAreaElement) {
|
||||
export function useAutoHeight(element: HTMLTextAreaElement) {
|
||||
if (!element) return;
|
||||
element.style.height = 'auto';
|
||||
element.style.height = `${element.scrollHeight + 2}px`;
|
||||
element.style.height = `${element.scrollHeight}px`;
|
||||
}
|
||||
|
@ -1,9 +1,15 @@
|
||||
export function useResize(element: HTMLElement, callback: (width: number, height: number) => void) {
|
||||
export function useResize(
|
||||
element: HTMLElement,
|
||||
callback: (frame: { width: number; height: number }) => void
|
||||
) {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { width, height } = entry.contentRect;
|
||||
const { inlineSize, blockSize } = entry.borderBoxSize[0];
|
||||
|
||||
callback(width, height);
|
||||
callback({
|
||||
width: Math.round(inlineSize),
|
||||
height: Math.round(blockSize)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user