|
<script lang="ts"> |
|
import { createEventDispatcher } from "svelte"; |
|
|
|
import { platformIsMac } from "@graphite/utility-functions/platform"; |
|
|
|
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte"; |
|
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; |
|
|
|
const dispatch = createEventDispatcher<{ |
|
value: string; |
|
textFocused: undefined; |
|
textChanged: undefined; |
|
textChangeCanceled: undefined; |
|
}>(); |
|
|
|
let className = ""; |
|
export { className as class }; |
|
export let classes: Record<string, boolean> = {}; |
|
let styleName = ""; |
|
export { styleName as style }; |
|
export let styles: Record<string, string | number | undefined> = {}; |
|
export let value: string; |
|
export let label: string | undefined = undefined; |
|
export let spellcheck = false; |
|
export let disabled = false; |
|
export let textarea = false; |
|
export let tooltip: string | undefined = undefined; |
|
export let placeholder: string | undefined = undefined; |
|
export let hideContextMenu = false; |
|
|
|
let inputOrTextarea: HTMLInputElement | HTMLTextAreaElement | undefined; |
|
let id = String(Math.random()).substring(2); |
|
let macKeyboardLayout = platformIsMac(); |
|
|
|
$: inputValue = value; |
|
|
|
$: dispatch("value", inputValue); |
|
|
|
|
|
export function selectAllText(currentText: string) { |
|
if (!inputOrTextarea) return; |
|
|
|
// Setting the value directly is required to make the following `select()` call work |
|
inputOrTextarea.value = currentText; |
|
inputOrTextarea.select(); |
|
} |
|
|
|
export function focus() { |
|
inputOrTextarea?.focus(); |
|
} |
|
|
|
export function unFocus() { |
|
inputOrTextarea?.blur(); |
|
} |
|
|
|
export function getValue(): string { |
|
return inputOrTextarea?.value || ""; |
|
} |
|
|
|
export function setInputElementValue(value: string) { |
|
if (!inputOrTextarea) return; |
|
|
|
inputOrTextarea.value = value; |
|
} |
|
|
|
export function element(): HTMLInputElement | HTMLTextAreaElement | undefined { |
|
return inputOrTextarea; |
|
} |
|
|
|
function cancel() { |
|
dispatch("textChangeCanceled"); |
|
|
|
if (inputOrTextarea) preventEscapeClosingParentFloatingMenu(inputOrTextarea); |
|
} |
|
</script> |
|
|
|
<!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. --> |
|
<LayoutRow class={`field-input ${className}`} classes={{ disabled, ...classes }} style={styleName} {styles} {tooltip}> |
|
{#if !textarea} |
|
<input |
|
type="text" |
|
class:has-label={label} |
|
id={`field-input-${id}`} |
|
{spellcheck} |
|
{disabled} |
|
{placeholder} |
|
bind:this={inputOrTextarea} |
|
bind:value={inputValue} |
|
on:focus={() => dispatch("textFocused")} |
|
on:blur={() => dispatch("textChanged")} |
|
on:change={() => dispatch("textChanged")} |
|
on:keydown={(e) => e.key === "Enter" && dispatch("textChanged")} |
|
on:keydown={(e) => e.key === "Escape" && cancel()} |
|
on:pointerdown |
|
on:contextmenu={(e) => hideContextMenu && e.preventDefault()} |
|
data-input-element |
|
/> |
|
{:else} |
|
<textarea |
|
class:has-label={label} |
|
id={`field-input-${id}`} |
|
class="scrollable-y" |
|
data-scrollable-y |
|
{spellcheck} |
|
{disabled} |
|
bind:this={inputOrTextarea} |
|
bind:value={inputValue} |
|
on:focus={() => dispatch("textFocused")} |
|
on:blur={() => dispatch("textChanged")} |
|
on:change={() => dispatch("textChanged")} |
|
on:keydown={(e) => (macKeyboardLayout ? e.metaKey : e.ctrlKey) && e.key === "Enter" && dispatch("textChanged")} |
|
on:keydown={(e) => e.key === "Escape" && cancel()} |
|
on:pointerdown |
|
on:contextmenu={(e) => hideContextMenu && e.preventDefault()} |
|
/> |
|
{/if} |
|
{#if label} |
|
<label for={`field-input-${id}`} on:pointerdown>{label}</label> |
|
{/if} |
|
<slot /> |
|
</LayoutRow> |
|
|
|
<style lang="scss" global> |
|
.field-input { |
|
min-width: 80px; |
|
height: auto; |
|
position: relative; |
|
border-radius: 2px; |
|
background: var(--color-1-nearblack); |
|
flex-direction: row-reverse; |
|
|
|
label { |
|
flex: 0 0 auto; |
|
line-height: 18px; |
|
padding: 3px 0; |
|
padding-right: 4px; |
|
margin-left: 8px; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
white-space: nowrap; |
|
} |
|
|
|
&:not(.disabled) label { |
|
cursor: text; |
|
} |
|
|
|
input, |
|
textarea { |
|
flex: 1 1 100%; |
|
width: 0; |
|
min-width: 30px; |
|
height: 18px; |
|
line-height: 18px; |
|
margin: 0 8px; |
|
padding: 3px 0; |
|
outline: none; // Ok for input/textarea element |
|
border: none; |
|
background: none; |
|
color: var(--color-e-nearwhite); |
|
caret-color: var(--color-e-nearwhite); |
|
unicode-bidi: plaintext; |
|
|
|
&::selection { |
|
background-color: var(--color-4-dimgray); |
|
|
|
// Target only Safari |
|
@supports (background: -webkit-named-image(i)) { |
|
& { |
|
// Setting an alpha value opts out of Safari's "fancy" (but not visible on dark backgrounds) selection highlight rendering |
|
// https://stackoverflow.com/a/71753552/775283 |
|
background-color: rgba(var(--color-4-dimgray-rgb), calc(254 / 255)); |
|
} |
|
} |
|
} |
|
} |
|
|
|
input { |
|
&:not(:focus).has-label { |
|
text-align: right; |
|
margin-left: 0; |
|
margin-right: 8px; |
|
} |
|
|
|
&:focus { |
|
text-align: left; |
|
|
|
& + label { |
|
display: none; |
|
} |
|
} |
|
} |
|
|
|
textarea { |
|
min-height: calc(18px * 3); |
|
margin: 3px; |
|
padding: 0 5px; |
|
box-sizing: border-box; |
|
resize: vertical; |
|
} |
|
|
|
&.disabled { |
|
background: var(--color-2-mildblack); |
|
|
|
label, |
|
input, |
|
textarea { |
|
color: var(--color-8-uppergray); |
|
} |
|
|
|
input { |
|
// Disables drag-selecting the text, since `user-select: none` doesn't work for input elements |
|
pointer-events: none; |
|
} |
|
} |
|
} |
|
</style> |
|
|