Add the TextInput widget (#518)

* Separate out the number input from a generic field input

* Add TextInput widget

Co-authored-by: mfish33 <32677537+mfish33@users.noreply.github.com>
This commit is contained in:
Keavon Chambers
2022-02-09 12:16:44 -08:00
co-authored by mfish33
parent d9fbcd0092
commit 99bf6c496f
17 changed files with 278 additions and 140 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
API which is required for using the editor. However, you can still explore the user interface. API which is required for using the editor. However, you can still explore the user interface.
</p> </p>
<LayoutRow> <LayoutRow>
<button class="unsupported-modal-button" @click="closeModal()">I understand, let's just see the interface</button> <button class="unsupported-modal-button" @click="() => closeModal()">I understand, let's just see the interface</button>
</LayoutRow> </LayoutRow>
</LayoutCol> </LayoutCol>
</div> </div>
+5 -4
View File
@@ -29,7 +29,7 @@
</PopoverButton> </PopoverButton>
</LayoutRow> </LayoutRow>
<LayoutRow class="layer-tree" :scrollableY="true"> <LayoutRow class="layer-tree" :scrollableY="true">
<LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="(e) => draggable && updateInsertLine(e)" @dragend="draggable && drop()"> <LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="(e) => draggable && updateInsertLine(e)" @dragend="() => draggable && drop()">
<LayoutRow <LayoutRow
class="layer-row" class="layer-row"
v-for="(listing, index) in layers" v-for="(listing, index) in layers"
@@ -69,14 +69,15 @@
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeTypeFolder'" title="Folder" /> <IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeTypeFolder'" title="Folder" />
<IconLabel v-else :icon="'NodeTypePath'" title="Path" /> <IconLabel v-else :icon="'NodeTypePath'" title="Path" />
</LayoutRow> </LayoutRow>
<LayoutRow class="layer-name" @dblclick="onEditLayerName(listing)"> <LayoutRow class="layer-name" @dblclick="() => onEditLayerName(listing)">
<input <input
data-text-input
type="text" type="text"
:value="listing.entry.name" :value="listing.entry.name"
:placeholder="listing.entry.layer_type" :placeholder="listing.entry.layer_type"
:disabled="!listing.editingName" :disabled="!listing.editingName"
@change="(e) => onEditLayerNameChange(listing, e.target)" @change="(e) => onEditLayerNameChange(listing, e.target)"
@blur="onEditLayerNameDeselect(listing)" @blur="() => onEditLayerNameDeselect(listing)"
@keydown.enter="(e) => onEditLayerNameChange(listing, e.target)" @keydown.enter="(e) => onEditLayerNameChange(listing, e.target)"
@keydown.escape="onEditLayerNameDeselect(listing)" @keydown.escape="onEditLayerNameDeselect(listing)"
/> />
@@ -379,7 +380,7 @@ export default defineComponent({
listing.editingName = true; listing.editingName = true;
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement; const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement;
this.$nextTick(() => { this.$nextTick(() => {
(tree.querySelector("input:not([disabled])") as HTMLInputElement).select(); (tree.querySelector("[data-text-input]:not([disabled])") as HTMLInputElement).select();
}); });
}, },
async onEditLayerNameChange(listing: LayerListingInfo, inputElement: EventTarget | null) { async onEditLayerNameChange(listing: LayerListingInfo, inputElement: EventTarget | null) {
@@ -13,6 +13,7 @@
:incrementCallbackIncrease="() => updateLayout(component.widget_id, 'Increment')" :incrementCallbackIncrease="() => updateLayout(component.widget_id, 'Increment')"
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')" :incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
/> />
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @update:value="(value: number) => updateLayout(component.widget_id, value)" />
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" /> <IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" /> <OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" /> <RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
@@ -40,6 +41,7 @@ import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue"; import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue"; import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
import RadioInput from "@/components/widgets/inputs/RadioInput.vue"; import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
import TextInput from "@/components/widgets/inputs/TextInput.vue";
import Separator from "@/components/widgets/separators/Separator.vue"; import Separator from "@/components/widgets/separators/Separator.vue";
export default defineComponent({ export default defineComponent({
@@ -57,6 +59,7 @@ export default defineComponent({
Separator, Separator,
PopoverButton, PopoverButton,
NumberInput, NumberInput,
TextInput,
IconButton, IconButton,
OptionalInput, OptionalInput,
RadioInput, RadioInput,
@@ -47,9 +47,7 @@ const WidgetSection = defineComponent({
throw new Error("Layout row type does not exist"); throw new Error("Layout row type does not exist");
}, },
}, },
components: { components: { WidgetRow },
WidgetRow,
},
}); });
export default WidgetSection; export default WidgetSection;
</script> </script>
@@ -131,6 +131,7 @@ type ColorPickerState = "Idle" | "MoveHue" | "MoveOpacity" | "MoveSaturation";
// TODO: Such as removing the `picker*` data variables and reducing the number of functions which call each other in weird, non-obvious ways. // TODO: Such as removing the `picker*` data variables and reducing the number of functions which call each other in weird, non-obvious ways.
export default defineComponent({ export default defineComponent({
emits: ["update:color"],
props: { props: {
color: { type: Object as PropType<RGBA>, required: true }, color: { type: Object as PropType<RGBA>, required: true },
}, },
@@ -7,9 +7,9 @@
:key="entryIndex" :key="entryIndex"
class="row" class="row"
:class="{ open: isMenuEntryOpen(entry), active: entry === activeEntry }" :class="{ open: isMenuEntryOpen(entry), active: entry === activeEntry }"
@click="handleEntryClick(entry)" @click="() => handleEntryClick(entry)"
@pointerenter="handleEntryPointerEnter(entry)" @pointerenter="() => handleEntryPointerEnter(entry)"
@pointerleave="handleEntryPointerLeave(entry)" @pointerleave="() => handleEntryPointerLeave(entry)"
:data-hover-menu-spawner-extend="entry.children && []" :data-hover-menu-spawner-extend="entry.children && []"
> >
<CheckboxInput v-if="entry.checkbox" v-model:checked="entry.checked" :outlineStyle="true" class="entry-checkbox" /> <CheckboxInput v-if="entry.checkbox" v-model:checked="entry.checked" :outlineStyle="true" class="entry-checkbox" />
@@ -160,6 +160,10 @@ const KEYBOARD_LOCK_USE_FULLSCREEN = "This hotkey is reserved by the browser, bu
const KEYBOARD_LOCK_SWITCH_BROWSER = "This hotkey is reserved by the browser, but becomes available in Chrome, Edge, and Opera which support the Keyboard.lock() API"; const KEYBOARD_LOCK_SWITCH_BROWSER = "This hotkey is reserved by the browser, but becomes available in Chrome, Edge, and Opera which support the Keyboard.lock() API";
const MenuList = defineComponent({ const MenuList = defineComponent({
emits: {
"update:activeEntry": null,
widthChanged: (width: number) => typeof width === "number",
},
inject: ["fullscreen"], inject: ["fullscreen"],
props: { props: {
direction: { type: String as PropType<MenuDirection>, default: "Bottom" }, direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
@@ -88,6 +88,7 @@ import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue"; import IconLabel from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({ export default defineComponent({
emits: ["update:checked"],
data() { data() {
return { return {
id: `${Math.random()}`.substring(2), id: `${Math.random()}`.substring(2),
@@ -98,6 +98,7 @@ declare global {
} }
export default defineComponent({ export default defineComponent({
emits: ["update:selectedIndex"],
props: { props: {
menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true }, menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
selectedIndex: { type: Number as PropType<number>, required: true }, selectedIndex: { type: Number as PropType<number>, required: true },
@@ -0,0 +1,116 @@
<!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. -->
<template>
<LayoutRow class="field-input" :class="{ disabled }">
<input
:class="{ 'has-label': label }"
:id="`field-input-${id}`"
ref="input"
type="text"
v-model="inputValue"
:spellcheck="spellcheck"
:disabled="disabled"
@focus="() => $emit('textFocused')"
@blur="() => $emit('textChanged')"
@change="() => $emit('textChanged')"
@keydown.enter="() => $emit('textChanged')"
@keydown.esc="() => $emit('cancelTextChange')"
/>
<label v-if="label" :for="`field-input-${id}`">{{ label }}</label>
<slot></slot>
</LayoutRow>
</template>
<style lang="scss">
.field-input {
min-width: 80px;
height: 24px;
position: relative;
border-radius: 2px;
background: var(--color-1-nearblack);
overflow: hidden;
flex-direction: row-reverse;
label {
flex: 1 1 100%;
line-height: 18px;
margin-left: 8px;
padding: 3px 0;
overflow: hidden;
text-overflow: ellipsis;
}
&:not(.disabled) label {
cursor: text;
}
input {
flex: 1 1 100%;
width: 0;
min-width: 30px;
height: 18px;
line-height: 18px;
margin: 0 8px;
padding: 3px 0;
outline: none;
border: none;
background: none;
color: var(--color-e-nearwhite);
text-align: center;
&:not(:focus).has-label {
text-align: right;
margin-left: 0;
margin-right: 8px;
}
&:focus {
text-align: left;
& + label {
display: none;
}
}
}
&.disabled {
background: var(--color-2-mildblack);
label,
input {
color: var(--color-8-uppergray);
}
}
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
export default defineComponent({
emits: ["update:value", "textFocused", "textChanged", "cancelTextChange"],
props: {
value: { type: String as PropType<string>, required: true },
label: { type: String as PropType<string>, required: false },
spellcheck: { type: Boolean as PropType<boolean>, default: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
},
data() {
return {
id: `${Math.random()}`.substring(2),
};
},
computed: {
inputValue: {
get() {
return this.value;
},
set(value: string) {
this.$emit("update:value", value);
},
},
},
components: { LayoutRow },
});
</script>
@@ -1,12 +1,12 @@
<template> <template>
<div class="menu-bar-input"> <div class="menu-bar-input">
<div class="entry-container"> <div class="entry-container">
<div @click="visitWebsite('https://www.graphite.design')" class="entry"> <div @click="() => visitWebsite('https://www.graphite.design')" class="entry">
<IconLabel :icon="'GraphiteLogo'" /> <IconLabel :icon="'GraphiteLogo'" />
</div> </div>
</div> </div>
<div class="entry-container" v-for="(entry, index) in menuEntries" :key="index"> <div class="entry-container" v-for="(entry, index) in menuEntries" :key="index">
<div @click="handleEntryClick(entry)" class="entry" :class="{ open: entry.ref && entry.ref.isOpen() }" data-hover-menu-spawner> <div @click="() => handleEntryClick(entry)" class="entry" :class="{ open: entry.ref && entry.ref.isOpen() }" data-hover-menu-spawner>
<IconLabel :icon="entry.icon" v-if="entry.icon" /> <IconLabel :icon="entry.icon" v-if="entry.icon" />
<span v-if="entry.label">{{ entry.label }}</span> <span v-if="entry.label">{{ entry.label }}</span>
</div> </div>
@@ -1,73 +1,24 @@
<template> <template>
<LayoutRow class="number-input" :class="{ disabled }"> <FieldInput
<input class="number-input"
:class="{ 'has-label': label }" v-model:value="text"
:id="`number-input-${id}`" :label="label"
type="text" :spellcheck="false"
spellcheck="false" :disabled="disabled"
v-model="text" @textFocused="() => onTextFocused()"
@change="onTextChanged()" @textChanged="() => onTextChanged()"
@keydown.esc="onCancelTextChange" @cancelTextChange="() => onCancelTextChange()"
ref="input" ref="fieldInput"
:disabled="disabled" >
/> <button v-if="!Number.isNaN(value)" class="arrow left" @click="() => onIncrement('Decrease')"></button>
<label v-if="label" :for="`number-input-${id}`">{{ label }}</label> <button v-if="!Number.isNaN(value)" class="arrow right" @click="() => onIncrement('Increase')"></button>
<button v-if="!Number.isNaN(value)" class="arrow left" @click="onIncrement('Decrease')"></button> </FieldInput>
<button v-if="!Number.isNaN(value)" class="arrow right" @click="onIncrement('Increase')"></button>
</LayoutRow>
</template> </template>
<style lang="scss"> <style lang="scss">
.number-input { .number-input {
min-width: 80px; input:focus ~ .arrow {
height: 24px; display: none;
position: relative;
border-radius: 2px;
background: var(--color-1-nearblack);
overflow: hidden;
flex-direction: row-reverse;
label {
flex: 1 1 100%;
line-height: 18px;
margin-left: 8px;
padding: 3px 0;
overflow: hidden;
text-overflow: ellipsis;
}
&:not(.disabled) label {
cursor: text;
}
input {
flex: 1 1 100%;
width: 0;
min-width: 30px;
height: 18px;
line-height: 18px;
margin: 0 8px;
padding: 3px 0;
outline: none;
border: none;
background: none;
color: var(--color-e-nearwhite);
text-align: center;
&:not(:focus).has-label {
text-align: right;
margin-left: 0;
margin-right: 8px;
}
&:focus {
text-align: left;
& + label,
& ~ .arrow {
display: none;
}
}
} }
&:not(:hover) .arrow { &:not(:hover) .arrow {
@@ -127,17 +78,8 @@
} }
} }
&.disabled { &.disabled .arrow {
background: var(--color-2-mildblack); display: none;
label,
input {
color: var(--color-8-uppergray);
}
.arrow {
display: none;
}
} }
} }
</style> </style>
@@ -147,9 +89,10 @@ import { defineComponent, PropType } from "vue";
import { IncrementBehavior, IncrementDirection } from "@/utilities/widgets"; import { IncrementBehavior, IncrementDirection } from "@/utilities/widgets";
import LayoutRow from "@/components/layout/LayoutRow.vue"; import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
export default defineComponent({ export default defineComponent({
emits: ["update:value"],
props: { props: {
value: { type: Number as PropType<number>, required: true }, value: { type: Number as PropType<number>, required: true },
min: { type: Number as PropType<number>, required: false }, min: { type: Number as PropType<number>, required: false },
@@ -167,9 +110,8 @@ export default defineComponent({
}, },
data() { data() {
return { return {
text: this.generateText(this.value), text: this.displayText(this.value),
editing: false, editing: false,
id: `${Math.random()}`.substring(2),
}; };
}, },
methods: { methods: {
@@ -177,8 +119,10 @@ export default defineComponent({
if (Number.isNaN(this.value)) this.text = ""; if (Number.isNaN(this.value)) this.text = "";
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`; else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
else this.text = `${this.value}${this.unit}`; else this.text = `${this.value}${this.unit}`;
this.editing = true; this.editing = true;
const inputElement = this.$refs.input as HTMLInputElement;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
// Setting the value directly is required to make `inputElement.select()` work // Setting the value directly is required to make `inputElement.select()` work
inputElement.value = this.text; inputElement.value = this.text;
inputElement.select(); inputElement.select();
@@ -187,59 +131,66 @@ export default defineComponent({
// enter key (via the `change` event) or when the <input> element is defocused (with the `blur` event binding) // enter key (via the `change` event) or when the <input> element is defocused (with the `blur` event binding)
onTextChanged() { onTextChanged() {
// The `inputElement.blur()` call at the bottom of this function causes itself to be run again, so this check skips a second run // The `inputElement.blur()` call at the bottom of this function causes itself to be run again, so this check skips a second run
if (!this.editing) return; if (this.editing) {
const newValue = parseFloat(this.text); const newValue = parseFloat(this.text);
this.updateValue(newValue); this.updateValue(newValue);
this.editing = false;
const inputElement = this.$refs.input as HTMLElement; this.editing = false;
inputElement.blur();
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
inputElement.blur();
}
}, },
onCancelTextChange() { onCancelTextChange() {
this.updateValue(NaN); this.updateValue(NaN);
this.editing = false; this.editing = false;
const inputElement = this.$refs.input as HTMLElement;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
inputElement.blur(); inputElement.blur();
}, },
onIncrement(direction: IncrementDirection) { onIncrement(direction: IncrementDirection) {
if (Number.isNaN(this.value)) return; if (Number.isNaN(this.value)) return;
switch (this.incrementBehavior) {
case "Add": { ({
Add: (): void => {
const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor; const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor;
this.updateValue(this.value + directionAddend); this.updateValue(this.value + directionAddend);
break; },
} Multiply: (): void => {
case "Multiply": {
const directionMultiplier = direction === "Increase" ? this.incrementFactor : 1 / this.incrementFactor; const directionMultiplier = direction === "Increase" ? this.incrementFactor : 1 / this.incrementFactor;
this.updateValue(this.value * directionMultiplier); this.updateValue(this.value * directionMultiplier);
break; },
} Callback: (): void => {
case "Callback": {
if (direction === "Increase" && this.incrementCallbackIncrease) this.incrementCallbackIncrease(); if (direction === "Increase" && this.incrementCallbackIncrease) this.incrementCallbackIncrease();
if (direction === "Decrease" && this.incrementCallbackDecrease) this.incrementCallbackDecrease(); if (direction === "Decrease" && this.incrementCallbackDecrease) this.incrementCallbackDecrease();
break; },
} None: (): void => undefined,
default: }[this.incrementBehavior]());
break;
}
}, },
updateValue(newValue: number) { updateValue(newValue: number) {
let sanitized = newValue;
const invalid = Number.isNaN(newValue); const invalid = Number.isNaN(newValue);
let sanitized = newValue;
if (invalid) sanitized = this.value; if (invalid) sanitized = this.value;
if (this.isInteger) sanitized = Math.round(sanitized); if (this.isInteger) sanitized = Math.round(sanitized);
if (typeof this.min === "number" && !Number.isNaN(this.min)) sanitized = Math.max(sanitized, this.min); if (typeof this.min === "number" && !Number.isNaN(this.min)) sanitized = Math.max(sanitized, this.min);
if (typeof this.max === "number" && !Number.isNaN(this.max)) sanitized = Math.min(sanitized, this.max); if (typeof this.max === "number" && !Number.isNaN(this.max)) sanitized = Math.min(sanitized, this.max);
if (!invalid) this.$emit("update:value", sanitized); if (!invalid) this.$emit("update:value", sanitized);
this.text = this.generateText(sanitized);
this.text = this.displayText(sanitized);
}, },
generateText(value: number): string { displayText(value: number): string {
// Find the amount of digits on the left side of the decimal // Find the amount of digits on the left side of the decimal
// 10.25 == 2 // 10.25 == 2
// 1.23 == 1 // 1.23 == 1
// 0.23 == 0 (Reason for the slightly more complicated code) // 0.23 == 0 (reason for the slightly more complicated code)
const leftSideDigits = Math.max(Math.floor(value).toString().length, 0) * Math.sign(value); const leftSideDigits = Math.max(Math.floor(value).toString().length, 0) * Math.sign(value);
const roundingPower = 10 ** Math.max(this.displayDecimalPlaces - leftSideDigits, 0); const roundingPower = 10 ** Math.max(this.displayDecimalPlaces - leftSideDigits, 0);
const displayValue = Math.round(value * roundingPower) / roundingPower; const displayValue = Math.round(value * roundingPower) / roundingPower;
return `${displayValue}${this.unit}`; return `${displayValue}${this.unit}`;
}, },
}, },
@@ -250,23 +201,15 @@ export default defineComponent({
this.text = "-"; this.text = "-";
return; return;
} }
// The simple `clamp()` function can't be used here since `undefined` values need to be boundless // The simple `clamp()` function can't be used here since `undefined` values need to be boundless
let sanitized = newValue; let sanitized = newValue;
if (typeof this.min === "number") sanitized = Math.max(sanitized, this.min); if (typeof this.min === "number") sanitized = Math.max(sanitized, this.min);
if (typeof this.max === "number") sanitized = Math.min(sanitized, this.max); if (typeof this.max === "number") sanitized = Math.min(sanitized, this.max);
this.text = this.generateText(sanitized);
this.text = this.displayText(sanitized);
}, },
}, },
mounted() { components: { FieldInput },
const inputElement = this.$refs.input as HTMLInputElement;
inputElement.addEventListener("focus", this.onTextFocused);
inputElement.addEventListener("blur", this.onTextChanged);
},
beforeUnmount() {
const inputElement = this.$refs.input as HTMLInputElement;
inputElement.removeEventListener("focus", this.onTextFocused);
inputElement.removeEventListener("blur", this.onTextChanged);
},
components: { LayoutRow },
}); });
</script> </script>
@@ -41,6 +41,7 @@ import LayoutRow from "@/components/layout/LayoutRow.vue";
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue"; import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
export default defineComponent({ export default defineComponent({
emits: ["update:checked"],
props: { props: {
checked: { type: Boolean as PropType<boolean>, required: true }, checked: { type: Boolean as PropType<boolean>, required: true },
icon: { type: String as PropType<IconName>, default: "Checkmark" }, icon: { type: String as PropType<IconName>, default: "Checkmark" },
@@ -1,6 +1,6 @@
<template> <template>
<LayoutRow class="radio-input"> <LayoutRow class="radio-input">
<button :class="{ active: index === selectedIndex }" v-for="(entry, index) in entries" :key="index" @click="handleEntryClick(entry)" :title="entry.tooltip"> <button :class="{ active: index === selectedIndex }" v-for="(entry, index) in entries" :key="index" @click="() => handleEntryClick(entry)" :title="entry.tooltip">
<IconLabel v-if="entry.icon" :icon="entry.icon" /> <IconLabel v-if="entry.icon" :icon="entry.icon" />
<TextLabel v-if="entry.label">{{ entry.label }}</TextLabel> <TextLabel v-if="entry.label">{{ entry.label }}</TextLabel>
</button> </button>
@@ -81,6 +81,7 @@ export interface RadioEntryData {
export type RadioEntries = RadioEntryData[]; export type RadioEntries = RadioEntryData[];
export default defineComponent({ export default defineComponent({
emits: ["update:selectedIndex"],
props: { props: {
entries: { type: Array as PropType<RadioEntries>, required: true }, entries: { type: Array as PropType<RadioEntries>, required: true },
selectedIndex: { type: Number as PropType<number>, required: true }, selectedIndex: { type: Number as PropType<number>, required: true },
@@ -0,0 +1,68 @@
<template>
<FieldInput
class="text-input"
v-model:value="text"
:label="label"
:spellcheck="true"
:disabled="disabled"
@textFocused="() => onTextFocused()"
@textChanged="() => onTextChanged()"
@cancelTextChange="() => onCancelTextChange()"
ref="fieldInput"
></FieldInput>
</template>
<style lang="scss"></style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
export default defineComponent({
emits: ["update:value"],
props: {
value: { type: String as PropType<string>, required: true },
label: { type: String as PropType<string>, required: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
},
data() {
return {
editing: false,
};
},
computed: {
text: {
get() {
return this.value;
},
set(value: string) {
this.$emit("update:value", value);
},
},
},
methods: {
onTextFocused() {
this.editing = true;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
// Setting the value directly is required to make `inputElement.select()` work
inputElement.value = this.text;
inputElement.select();
},
// Called only when `value` is changed from the <input> element via user input and committed, either with the
// enter key (via the `change` event) or when the <input> element is defocused (with the `blur` event binding)
onTextChanged() {
// The `inputElement.blur()` call in `onCancelTextChange()` causes itself to be run again, so this if statement skips a second run
if (this.editing) this.onCancelTextChange();
},
onCancelTextChange() {
this.editing = false;
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
inputElement.blur();
},
},
components: { FieldInput },
});
</script>
@@ -1,10 +1,10 @@
<template> <template>
<div class="persistent-scrollbar" :class="direction.toLowerCase()"> <div class="persistent-scrollbar" :class="direction.toLowerCase()">
<button class="arrow decrease" @pointerdown="changePosition(-50)"></button> <button class="arrow decrease" @pointerdown="() => changePosition(-50)"></button>
<div class="scroll-track" ref="scrollTrack" @pointerdown="(e) => grabArea(e)"> <div class="scroll-track" ref="scrollTrack" @pointerdown="(e) => grabArea(e)">
<div class="scroll-thumb" @pointerdown="(e) => grabHandle(e)" :class="{ dragging }" ref="handle" :style="[thumbStart, thumbEnd, sides]"></div> <div class="scroll-thumb" @pointerdown="(e) => grabHandle(e)" :class="{ dragging }" ref="handle" :style="[thumbStart, thumbEnd, sides]"></div>
</div> </div>
<button class="arrow increase" @click="changePosition(50)"></button> <button class="arrow increase" @click="() => changePosition(50)"></button>
</div> </div>
</template> </template>
@@ -121,6 +121,10 @@ const handleToTrack = (handleLen: number, handlePos: number): number => lerp(han
const pointerPosition = (direction: ScrollbarDirection, e: PointerEvent): number => (direction === "Vertical" ? e.clientY : e.clientX); const pointerPosition = (direction: ScrollbarDirection, e: PointerEvent): number => (direction === "Vertical" ? e.clientY : e.clientX);
export default defineComponent({ export default defineComponent({
emits: {
"update:handlePosition": null,
pressTrack: (pointerOffset: number) => typeof pointerOffset === "number",
},
props: { props: {
direction: { type: String as PropType<ScrollbarDirection>, default: "Vertical" }, direction: { type: String as PropType<ScrollbarDirection>, default: "Vertical" },
handlePosition: { type: Number as PropType<number>, default: 0.5 }, handlePosition: { type: Number as PropType<number>, default: 0.5 },
@@ -118,22 +118,18 @@ export default defineComponent({
nextSibling.style.flexGrow = (nextSiblingSize + mouseDelta).toString(); nextSibling.style.flexGrow = (nextSiblingSize + mouseDelta).toString();
previousSibling.style.flexGrow = (previousSiblingSize - mouseDelta).toString(); previousSibling.style.flexGrow = (previousSiblingSize - mouseDelta).toString();
window.dispatchEvent( window.dispatchEvent(new CustomEvent("resize", { detail: {} }));
new CustomEvent("resize", {
detail: {},
})
);
} }
document.addEventListener("pointermove", updatePosition);
function cleanup(event: PointerEvent): void { function cleanup(event: PointerEvent): void {
gutter.releasePointerCapture(event.pointerId); gutter.releasePointerCapture(event.pointerId);
document.removeEventListener("pointermove", updatePosition); document.removeEventListener("pointermove", updatePosition);
document.removeEventListener("pointerleave", cleanup); document.removeEventListener("pointerleave", cleanup);
document.removeEventListener("pointerup", cleanup); document.removeEventListener("pointerup", cleanup);
} }
document.addEventListener("pointermove", updatePosition);
document.addEventListener("pointerleave", cleanup); document.addEventListener("pointerleave", cleanup);
document.addEventListener("pointerup", cleanup); document.addEventListener("pointerup", cleanup);
}, },
+1 -1
View File
@@ -412,7 +412,7 @@ export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow
return Boolean((layoutRow as WidgetSection).layout); return Boolean((layoutRow as WidgetSection).layout);
} }
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput"; export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput" | "TextInput";
export interface Widget { export interface Widget {
kind: WidgetKind; kind: WidgetKind;