mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-27 10:18:11 +08:00
Move JS script to the top of each Vue file
This commit is contained in:
@@ -1,3 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { type IconName } from "@/utility-functions/icons";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:checked"],
|
||||
props: {
|
||||
checked: { type: Boolean as PropType<boolean>, default: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
icon: { type: String as PropType<IconName>, default: "Checkmark" },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: `${Math.random()}`.substring(2),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
displayIcon(): IconName {
|
||||
if (!this.checked && this.icon === "Checkmark") return "Empty12px";
|
||||
|
||||
return this.icon;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isChecked() {
|
||||
return this.checked;
|
||||
},
|
||||
toggleCheckboxFromLabel(e: KeyboardEvent) {
|
||||
const target = (e.target || undefined) as HTMLLabelElement | undefined;
|
||||
const previousSibling = (target?.previousSibling || undefined) as HTMLInputElement | undefined;
|
||||
previousSibling?.click();
|
||||
},
|
||||
},
|
||||
components: {
|
||||
IconLabel,
|
||||
LayoutRow,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutRow class="checkbox-input">
|
||||
<input
|
||||
@@ -80,48 +125,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { type IconName } from "@/utility-functions/icons";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:checked"],
|
||||
props: {
|
||||
checked: { type: Boolean as PropType<boolean>, default: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
icon: { type: String as PropType<IconName>, default: "Checkmark" },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: `${Math.random()}`.substring(2),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
displayIcon(): IconName {
|
||||
if (!this.checked && this.icon === "Checkmark") return "Empty12px";
|
||||
|
||||
return this.icon;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isChecked() {
|
||||
return this.checked;
|
||||
},
|
||||
toggleCheckboxFromLabel(e: KeyboardEvent) {
|
||||
const target = (e.target || undefined) as HTMLLabelElement | undefined;
|
||||
const previousSibling = (target?.previousSibling || undefined) as HTMLInputElement | undefined;
|
||||
previousSibling?.click();
|
||||
},
|
||||
},
|
||||
components: {
|
||||
IconLabel,
|
||||
LayoutRow,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { Color } from "@/wasm-communication/messages";
|
||||
|
||||
import ColorPicker from "@/components/floating-menus/ColorPicker.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:value", "update:open"],
|
||||
props: {
|
||||
value: { type: Color as PropType<Color>, required: true },
|
||||
noTransparency: { type: Boolean as PropType<boolean>, default: false }, // TODO: Rename to allowTransparency, also implement allowNone
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false }, // TODO: Design and implement
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
|
||||
// Bound through `v-model`
|
||||
// TODO: See if this should be made to follow the pattern of DropdownInput.vue so this could be removed
|
||||
open: { type: Boolean as PropType<boolean>, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// Called only when `open` is changed from outside this component (with v-model)
|
||||
open(newOpen: boolean) {
|
||||
this.isOpen = newOpen;
|
||||
},
|
||||
isOpen(newIsOpen: boolean) {
|
||||
this.$emit("update:open", newIsOpen);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
colorPickerUpdated(color: Color) {
|
||||
this.$emit("update:value", color);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
chip() {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ColorPicker,
|
||||
LayoutRow,
|
||||
TextLabel,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutRow class="color-input" :class="{ 'sharp-right-corners': sharpRightCorners }" :title="tooltip">
|
||||
<button
|
||||
@@ -77,57 +131,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { Color } from "@/wasm-communication/messages";
|
||||
|
||||
import ColorPicker from "@/components/floating-menus/ColorPicker.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:value", "update:open"],
|
||||
props: {
|
||||
value: { type: Color as PropType<Color>, required: true },
|
||||
noTransparency: { type: Boolean as PropType<boolean>, default: false }, // TODO: Rename to allowTransparency, also implement allowNone
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false }, // TODO: Design and implement
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
|
||||
// Bound through `v-model`
|
||||
// TODO: See if this should be made to follow the pattern of DropdownInput.vue so this could be removed
|
||||
open: { type: Boolean as PropType<boolean>, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// Called only when `open` is changed from outside this component (with v-model)
|
||||
open(newOpen: boolean) {
|
||||
this.isOpen = newOpen;
|
||||
},
|
||||
isOpen(newIsOpen: boolean) {
|
||||
this.$emit("update:open", newIsOpen);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
colorPickerUpdated(color: Color) {
|
||||
this.$emit("update:value", color);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
chip() {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ColorPicker,
|
||||
LayoutRow,
|
||||
TextLabel,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,80 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType, toRaw } from "vue";
|
||||
|
||||
import { type MenuListEntry } from "@/wasm-communication/messages";
|
||||
|
||||
import MenuList from "@/components/floating-menus/MenuList.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
const DASH_ENTRY = { label: "-" };
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:selectedIndex"],
|
||||
props: {
|
||||
entries: { type: Array as PropType<MenuListEntry[][]>, required: true },
|
||||
selectedIndex: { type: Number as PropType<number>, required: false }, // When not provided, a dash is displayed
|
||||
drawIcon: { type: Boolean as PropType<boolean>, default: false },
|
||||
interactive: { type: Boolean as PropType<boolean>, default: true },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeEntry: this.makeActiveEntry(this.selectedIndex),
|
||||
activeEntrySkipWatcher: false,
|
||||
open: false,
|
||||
minWidth: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// Called only when `selectedIndex` is changed from outside this component (with v-model)
|
||||
selectedIndex() {
|
||||
this.activeEntrySkipWatcher = true;
|
||||
this.activeEntry = this.makeActiveEntry();
|
||||
},
|
||||
// Called when `activeEntry` is changed by the `v-model` on this component's MenuList component, or by the `selectedIndex()` watcher above (but we want to skip that case)
|
||||
activeEntry(newActiveEntry: MenuListEntry) {
|
||||
if (this.activeEntrySkipWatcher) {
|
||||
this.activeEntrySkipWatcher = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// `toRaw()` pulls it out of the Vue proxy
|
||||
if (toRaw(newActiveEntry) === DASH_ENTRY) return;
|
||||
|
||||
this.$emit("update:selectedIndex", this.entries.flat().indexOf(newActiveEntry));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
makeActiveEntry(): MenuListEntry {
|
||||
const entries = this.entries.flat();
|
||||
|
||||
if (this.selectedIndex !== undefined && this.selectedIndex >= 0 && this.selectedIndex < entries.length) {
|
||||
return entries[this.selectedIndex];
|
||||
}
|
||||
return DASH_ENTRY;
|
||||
},
|
||||
keydown(e: KeyboardEvent) {
|
||||
(this.$refs.menuList as typeof MenuList | undefined)?.keydown(e, false);
|
||||
},
|
||||
unFocusDropdownBox(e: FocusEvent) {
|
||||
const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]");
|
||||
const self: HTMLDivElement | undefined = this.$el;
|
||||
if (blurTarget !== self) this.open = false;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
IconLabel,
|
||||
LayoutRow,
|
||||
MenuList,
|
||||
TextLabel,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutRow class="dropdown-input" data-dropdown-input>
|
||||
<LayoutRow
|
||||
@@ -95,80 +172,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType, toRaw } from "vue";
|
||||
|
||||
import { type MenuListEntry } from "@/wasm-communication/messages";
|
||||
|
||||
import MenuList from "@/components/floating-menus/MenuList.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
const DASH_ENTRY = { label: "-" };
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:selectedIndex"],
|
||||
props: {
|
||||
entries: { type: Array as PropType<MenuListEntry[][]>, required: true },
|
||||
selectedIndex: { type: Number as PropType<number>, required: false }, // When not provided, a dash is displayed
|
||||
drawIcon: { type: Boolean as PropType<boolean>, default: false },
|
||||
interactive: { type: Boolean as PropType<boolean>, default: true },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeEntry: this.makeActiveEntry(this.selectedIndex),
|
||||
activeEntrySkipWatcher: false,
|
||||
open: false,
|
||||
minWidth: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// Called only when `selectedIndex` is changed from outside this component (with v-model)
|
||||
selectedIndex() {
|
||||
this.activeEntrySkipWatcher = true;
|
||||
this.activeEntry = this.makeActiveEntry();
|
||||
},
|
||||
// Called when `activeEntry` is changed by the `v-model` on this component's MenuList component, or by the `selectedIndex()` watcher above (but we want to skip that case)
|
||||
activeEntry(newActiveEntry: MenuListEntry) {
|
||||
if (this.activeEntrySkipWatcher) {
|
||||
this.activeEntrySkipWatcher = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// `toRaw()` pulls it out of the Vue proxy
|
||||
if (toRaw(newActiveEntry) === DASH_ENTRY) return;
|
||||
|
||||
this.$emit("update:selectedIndex", this.entries.flat().indexOf(newActiveEntry));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
makeActiveEntry(): MenuListEntry {
|
||||
const entries = this.entries.flat();
|
||||
|
||||
if (this.selectedIndex !== undefined && this.selectedIndex >= 0 && this.selectedIndex < entries.length) {
|
||||
return entries[this.selectedIndex];
|
||||
}
|
||||
return DASH_ENTRY;
|
||||
},
|
||||
keydown(e: KeyboardEvent) {
|
||||
(this.$refs.menuList as typeof MenuList | undefined)?.keydown(e, false);
|
||||
},
|
||||
unFocusDropdownBox(e: FocusEvent) {
|
||||
const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]");
|
||||
const self: HTMLDivElement | undefined = this.$el;
|
||||
if (blurTarget !== self) this.open = false;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
IconLabel,
|
||||
LayoutRow,
|
||||
MenuList,
|
||||
TextLabel,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { platformIsMac } from "@/utility-functions/platform";
|
||||
|
||||
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 },
|
||||
textarea: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
placeholder: { type: String as PropType<string>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: `${Math.random()}`.substring(2),
|
||||
macKeyboardLayout: platformIsMac(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// Select (highlight) all the text. For technical reasons, it is necessary to pass the current text.
|
||||
selectAllText(currentText: string) {
|
||||
const inputElement = this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined;
|
||||
if (!inputElement) return;
|
||||
|
||||
// Setting the value directly is required to make `inputElement.select()` work
|
||||
inputElement.value = currentText;
|
||||
|
||||
inputElement.select();
|
||||
},
|
||||
unFocus() {
|
||||
(this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined)?.blur();
|
||||
},
|
||||
getInputElementValue(): string | undefined {
|
||||
return (this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined)?.value;
|
||||
},
|
||||
setInputElementValue(value: string) {
|
||||
const inputElement = this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined;
|
||||
if (inputElement) inputElement.value = value;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inputValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value: string) {
|
||||
this.$emit("update:value", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
components: { LayoutRow },
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 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, 'sharp-right-corners': sharpRightCorners }" :title="tooltip">
|
||||
@@ -131,64 +192,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { platformIsMac } from "@/utility-functions/platform";
|
||||
|
||||
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 },
|
||||
textarea: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
placeholder: { type: String as PropType<string>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: `${Math.random()}`.substring(2),
|
||||
macKeyboardLayout: platformIsMac(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// Select (highlight) all the text. For technical reasons, it is necessary to pass the current text.
|
||||
selectAllText(currentText: string) {
|
||||
const inputElement = this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined;
|
||||
if (!inputElement) return;
|
||||
|
||||
// Setting the value directly is required to make `inputElement.select()` work
|
||||
inputElement.value = currentText;
|
||||
|
||||
inputElement.select();
|
||||
},
|
||||
unFocus() {
|
||||
(this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined)?.blur();
|
||||
},
|
||||
getInputElementValue(): string | undefined {
|
||||
return (this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined)?.value;
|
||||
},
|
||||
setInputElementValue(value: string) {
|
||||
const inputElement = this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined;
|
||||
if (inputElement) inputElement.value = value;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inputValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value: string) {
|
||||
this.$emit("update:value", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
components: { LayoutRow },
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,84 +1,3 @@
|
||||
<!-- TODO: Combine this widget into the DropdownInput widget -->
|
||||
|
||||
<template>
|
||||
<LayoutRow class="font-input">
|
||||
<LayoutRow
|
||||
class="dropdown-box"
|
||||
:class="{ disabled, 'sharp-right-corners': sharpRightCorners }"
|
||||
:style="{ minWidth: `${minWidth}px` }"
|
||||
:title="tooltip"
|
||||
:tabindex="disabled ? -1 : 0"
|
||||
@click="toggleOpen"
|
||||
@keydown="keydown"
|
||||
data-floating-menu-spawner
|
||||
>
|
||||
<TextLabel class="dropdown-label">{{ activeEntry?.value || "" }}</TextLabel>
|
||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||
</LayoutRow>
|
||||
<MenuList
|
||||
v-model:activeEntry="activeEntry"
|
||||
v-model:open="open"
|
||||
:entries="[entries]"
|
||||
:minWidth="isStyle ? 0 : minWidth"
|
||||
:virtualScrollingEntryHeight="isStyle ? 0 : 20"
|
||||
:scrollableY="true"
|
||||
@naturalWidth="(newNaturalWidth: number) => (isStyle && (minWidth = newNaturalWidth))"
|
||||
ref="menuList"
|
||||
></MenuList>
|
||||
</LayoutRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.font-input {
|
||||
position: relative;
|
||||
|
||||
.dropdown-box {
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
background: var(--color-1-nearblack);
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
|
||||
.dropdown-label {
|
||||
margin: 0;
|
||||
margin-left: 8px;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
margin: 6px 2px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
span {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--color-2-mildblack);
|
||||
|
||||
span {
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-list .floating-menu-container .floating-menu-content {
|
||||
max-height: 400px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick, type PropType } from "vue";
|
||||
|
||||
@@ -187,3 +106,84 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- TODO: Combine this widget into the DropdownInput widget -->
|
||||
|
||||
<template>
|
||||
<LayoutRow class="font-input">
|
||||
<LayoutRow
|
||||
class="dropdown-box"
|
||||
:class="{ disabled, 'sharp-right-corners': sharpRightCorners }"
|
||||
:style="{ minWidth: `${minWidth}px` }"
|
||||
:title="tooltip"
|
||||
:tabindex="disabled ? -1 : 0"
|
||||
@click="toggleOpen"
|
||||
@keydown="keydown"
|
||||
data-floating-menu-spawner
|
||||
>
|
||||
<TextLabel class="dropdown-label">{{ activeEntry?.value || "" }}</TextLabel>
|
||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||
</LayoutRow>
|
||||
<MenuList
|
||||
v-model:activeEntry="activeEntry"
|
||||
v-model:open="open"
|
||||
:entries="[entries]"
|
||||
:minWidth="isStyle ? 0 : minWidth"
|
||||
:virtualScrollingEntryHeight="isStyle ? 0 : 20"
|
||||
:scrollableY="true"
|
||||
@naturalWidth="(newNaturalWidth: number) => (isStyle && (minWidth = newNaturalWidth))"
|
||||
ref="menuList"
|
||||
></MenuList>
|
||||
</LayoutRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.font-input {
|
||||
position: relative;
|
||||
|
||||
.dropdown-box {
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
background: var(--color-1-nearblack);
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
|
||||
.dropdown-label {
|
||||
margin: 0;
|
||||
margin-left: 8px;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
margin: 6px 2px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
span {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--color-2-mildblack);
|
||||
|
||||
span {
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-list .floating-menu-container .floating-menu-content {
|
||||
max-height: 400px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { currentDraggingElement } from "@/io-managers/drag";
|
||||
|
||||
import type { LayerType, LayerTypeData } from "@/wasm-communication/messages";
|
||||
import { layerTypeData } from "@/wasm-communication/messages";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:value"],
|
||||
props: {
|
||||
value: { type: String as PropType<string | undefined>, required: false },
|
||||
layerName: { type: String as PropType<string | undefined>, required: false },
|
||||
layerType: { type: String as PropType<LayerType | undefined>, required: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hoveringDrop: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
droppable() {
|
||||
return this.hoveringDrop && currentDraggingElement();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dragOver(e: DragEvent): void {
|
||||
this.hoveringDrop = true;
|
||||
|
||||
e.preventDefault();
|
||||
},
|
||||
dragLeave(): void {
|
||||
this.hoveringDrop = false;
|
||||
},
|
||||
drop(e: DragEvent): void {
|
||||
this.hoveringDrop = false;
|
||||
|
||||
const element = currentDraggingElement();
|
||||
const layerPath = element?.getAttribute("data-layer") || undefined;
|
||||
|
||||
if (layerPath) {
|
||||
e.preventDefault();
|
||||
|
||||
this.$emit("update:value", layerPath);
|
||||
}
|
||||
},
|
||||
clearLayer(): void {
|
||||
this.$emit("update:value", undefined);
|
||||
},
|
||||
layerTypeData(layerType: LayerType): LayerTypeData {
|
||||
return layerTypeData(layerType) || { name: "Error", icon: "Info" };
|
||||
},
|
||||
},
|
||||
components: {
|
||||
IconButton,
|
||||
IconLabel,
|
||||
LayoutRow,
|
||||
TextLabel,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutRow
|
||||
class="layer-reference-input"
|
||||
@@ -89,73 +159,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { currentDraggingElement } from "@/io-managers/drag";
|
||||
|
||||
import type { LayerType, LayerTypeData } from "@/wasm-communication/messages";
|
||||
import { layerTypeData } from "@/wasm-communication/messages";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:value"],
|
||||
props: {
|
||||
value: { type: String as PropType<string | undefined>, required: false },
|
||||
layerName: { type: String as PropType<string | undefined>, required: false },
|
||||
layerType: { type: String as PropType<LayerType | undefined>, required: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hoveringDrop: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
droppable() {
|
||||
return this.hoveringDrop && currentDraggingElement();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dragOver(e: DragEvent): void {
|
||||
this.hoveringDrop = true;
|
||||
|
||||
e.preventDefault();
|
||||
},
|
||||
dragLeave(): void {
|
||||
this.hoveringDrop = false;
|
||||
},
|
||||
drop(e: DragEvent): void {
|
||||
this.hoveringDrop = false;
|
||||
|
||||
const element = currentDraggingElement();
|
||||
const layerPath = element?.getAttribute("data-layer") || undefined;
|
||||
|
||||
if (layerPath) {
|
||||
e.preventDefault();
|
||||
|
||||
this.$emit("update:value", layerPath);
|
||||
}
|
||||
},
|
||||
clearLayer(): void {
|
||||
this.$emit("update:value", undefined);
|
||||
},
|
||||
layerTypeData(layerType: LayerType): LayerTypeData {
|
||||
return layerTypeData(layerType) || { name: "Error", icon: "Info" };
|
||||
},
|
||||
},
|
||||
components: {
|
||||
IconButton,
|
||||
IconLabel,
|
||||
LayoutRow,
|
||||
TextLabel,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,69 +1,3 @@
|
||||
<template>
|
||||
<div class="menu-bar-input" data-menu-bar-input>
|
||||
<div class="entry-container" v-for="(entry, index) in entries" :key="index">
|
||||
<div
|
||||
@click="(e: MouseEvent) => clickEntry(entry, e)"
|
||||
@blur="(e: FocusEvent) => unFocusEntry(entry, e)"
|
||||
@keydown="(e: KeyboardEvent) => entry.ref?.keydown(e, false)"
|
||||
class="entry"
|
||||
:class="{ open: entry.ref?.isOpen }"
|
||||
tabindex="0"
|
||||
:data-floating-menu-spawner="entry.children && entry.children.length > 0 ? '' : 'no-hover-transfer'"
|
||||
>
|
||||
<IconLabel v-if="entry.icon" :icon="entry.icon" />
|
||||
<TextLabel v-if="entry.label">{{ entry.label }}</TextLabel>
|
||||
</div>
|
||||
<MenuList
|
||||
v-if="entry.children && entry.children.length > 0"
|
||||
:open="entry.ref?.open || false"
|
||||
:entries="entry.children || []"
|
||||
:direction="'Bottom'"
|
||||
:minWidth="240"
|
||||
:drawIcon="true"
|
||||
:ref="(ref: MenuListInstance): void => (ref && (entry.ref = ref), undefined)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.menu-bar-input {
|
||||
display: flex;
|
||||
|
||||
.entry-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
padding: 0 8px;
|
||||
background: none;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
|
||||
svg {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
svg {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
@@ -152,3 +86,69 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu-bar-input" data-menu-bar-input>
|
||||
<div class="entry-container" v-for="(entry, index) in entries" :key="index">
|
||||
<div
|
||||
@click="(e: MouseEvent) => clickEntry(entry, e)"
|
||||
@blur="(e: FocusEvent) => unFocusEntry(entry, e)"
|
||||
@keydown="(e: KeyboardEvent) => entry.ref?.keydown(e, false)"
|
||||
class="entry"
|
||||
:class="{ open: entry.ref?.isOpen }"
|
||||
tabindex="0"
|
||||
:data-floating-menu-spawner="entry.children && entry.children.length > 0 ? '' : 'no-hover-transfer'"
|
||||
>
|
||||
<IconLabel v-if="entry.icon" :icon="entry.icon" />
|
||||
<TextLabel v-if="entry.label">{{ entry.label }}</TextLabel>
|
||||
</div>
|
||||
<MenuList
|
||||
v-if="entry.children && entry.children.length > 0"
|
||||
:open="entry.ref?.open || false"
|
||||
:entries="entry.children || []"
|
||||
:direction="'Bottom'"
|
||||
:minWidth="240"
|
||||
:drawIcon="true"
|
||||
:ref="(ref: MenuListInstance): void => (ref && (entry.ref = ref), undefined)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.menu-bar-input {
|
||||
display: flex;
|
||||
|
||||
.entry-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
padding: 0 8px;
|
||||
background: none;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
|
||||
svg {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
svg {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,240 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { type NumberInputMode, type NumberInputIncrementBehavior } from "@/wasm-communication/messages";
|
||||
|
||||
import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:value"],
|
||||
props: {
|
||||
// Label
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
|
||||
// Disabled
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
|
||||
// Value
|
||||
value: { type: Number as PropType<number>, required: false }, // When not provided, a dash is displayed
|
||||
min: { type: Number as PropType<number>, required: false },
|
||||
max: { type: Number as PropType<number>, required: false },
|
||||
isInteger: { type: Boolean as PropType<boolean>, default: false },
|
||||
|
||||
// Number presentation
|
||||
displayDecimalPlaces: { type: Number as PropType<number>, default: 3 },
|
||||
unit: { type: String as PropType<string>, default: "" },
|
||||
unitIsHiddenWhenEditing: { type: Boolean as PropType<boolean>, default: true },
|
||||
|
||||
// Mode behavior
|
||||
// "Increment" shows arrows and allows dragging left/right to change the value.
|
||||
// "Range" shows a range slider between some minimum and maximum value.
|
||||
mode: { type: String as PropType<NumberInputMode>, default: "Increment" },
|
||||
// When `mode` is "Increment", `step` is the multiplier or addend used with `incrementBehavior`.
|
||||
// When `mode` is "Range", `step` is the range slider's snapping increment if `isInteger` is `true`.
|
||||
step: { type: Number as PropType<number>, default: 1 },
|
||||
// `incrementBehavior` is only applicable with a `mode` of "Increment".
|
||||
// "Add"/"Multiply": The value is added or multiplied by `step`.
|
||||
// "None": the increment arrows are not shown.
|
||||
// "Callback": the functions `incrementCallbackIncrease` and `incrementCallbackDecrease` call custom behavior.
|
||||
incrementBehavior: { type: String as PropType<NumberInputIncrementBehavior>, default: "Add" },
|
||||
// `rangeMin` and `rangeMax` are only applicable with a `mode` of "Range".
|
||||
// They set the lower and upper values of the slider to drag between.
|
||||
rangeMin: { type: Number as PropType<number>, default: 0 },
|
||||
rangeMax: { type: Number as PropType<number>, default: 1 },
|
||||
|
||||
// Styling
|
||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
|
||||
// Callbacks
|
||||
incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false },
|
||||
incrementCallbackDecrease: { type: Function as PropType<() => void>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
text: this.displayText(this.value),
|
||||
editing: false,
|
||||
// Stays in sync with a binding to the actual input range slider element.
|
||||
rangeSliderValue: this.value !== undefined ? this.value : 0,
|
||||
// Value used to render the position of the fake slider when applicable, and length of the progress colored region to the slider's left.
|
||||
// This is the same as `rangeSliderValue` except in the "mousedown" state, when it has the previous location before the user's mousedown.
|
||||
rangeSliderValueAsRendered: this.value !== undefined ? this.value : 0,
|
||||
// "default": no interaction is happening.
|
||||
// "mousedown": the user has pressed down the mouse and might next decide to either drag left/right or release without dragging.
|
||||
// "dragging": the user is dragging the slider left/right.
|
||||
rangeSliderClickDragState: "default" as "default" | "mousedown" | "dragging",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sliderStepValue() {
|
||||
const step = this.step === undefined ? 1 : this.step;
|
||||
return this.isInteger ? step : "any";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sliderInput() {
|
||||
// Keep only 4 digits after the decimal point
|
||||
const ROUNDING_EXPONENT = 4;
|
||||
const ROUNDING_MAGNITUDE = 10 ** ROUNDING_EXPONENT;
|
||||
const roundedValue = Math.round(this.rangeSliderValue * ROUNDING_MAGNITUDE) / ROUNDING_MAGNITUDE;
|
||||
|
||||
// Exit if this is an extraneous event invocation that occurred after mouseup, which happens in Firefox
|
||||
if (this.value !== undefined && Math.abs(this.value - roundedValue) < 1 / ROUNDING_MAGNITUDE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The first event upon mousedown means we transition to a "mousedown" state
|
||||
if (this.rangeSliderClickDragState === "default") {
|
||||
this.rangeSliderClickDragState = "mousedown";
|
||||
|
||||
// Exit early because we don't want to use the value set by where on the track the user pressed
|
||||
return;
|
||||
}
|
||||
|
||||
// The second event upon mousedown that occurs by moving left or right means the user has committed to dragging the slider
|
||||
if (this.rangeSliderClickDragState === "mousedown") {
|
||||
this.rangeSliderClickDragState = "dragging";
|
||||
}
|
||||
|
||||
// If we're in a dragging state, we want to use the new slider value
|
||||
this.rangeSliderValueAsRendered = roundedValue;
|
||||
this.updateValue(roundedValue);
|
||||
},
|
||||
sliderPointerDown() {
|
||||
// We want to render the fake slider thumb at the old position, which is still the number held by `value`
|
||||
this.rangeSliderValueAsRendered = this.value || 0;
|
||||
|
||||
// Because an `input` event is fired right before or after this (depending on browser), that first
|
||||
// invocation will transition the state machine to `mousedown`. That's why we don't do it here.
|
||||
},
|
||||
sliderPointerUp() {
|
||||
// User clicked but didn't drag, so we focus the text input element
|
||||
if (this.rangeSliderClickDragState === "mousedown") {
|
||||
const fieldInput = this.$refs.fieldInput as typeof FieldInput | undefined;
|
||||
const inputElement = fieldInput?.$el.querySelector("[data-input-element]") as HTMLInputElement | undefined;
|
||||
if (!inputElement) return;
|
||||
|
||||
// Set the slider position back to the original position to undo the user moving it
|
||||
this.rangeSliderValue = this.rangeSliderValueAsRendered;
|
||||
|
||||
// Begin editing the number text field
|
||||
inputElement.focus();
|
||||
}
|
||||
|
||||
// Releasing the mouse means we can reset the state machine
|
||||
this.rangeSliderClickDragState = "default";
|
||||
},
|
||||
onTextFocused() {
|
||||
if (this.value === undefined) this.text = "";
|
||||
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
|
||||
else this.text = `${this.value}${unPluralize(this.unit, this.value)}`;
|
||||
|
||||
this.editing = true;
|
||||
|
||||
(this.$refs.fieldInput as typeof FieldInput | undefined)?.selectAllText(this.text);
|
||||
},
|
||||
// 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 unfocused (with the `blur` event binding)
|
||||
onTextChanged() {
|
||||
// The `unFocus()` call at the bottom of this function and in `onCancelTextChange()` causes this function to be run again, so this check skips a second run
|
||||
if (!this.editing) return;
|
||||
|
||||
const parsed = parseFloat(this.text);
|
||||
const newValue = Number.isNaN(parsed) ? undefined : parsed;
|
||||
|
||||
this.updateValue(newValue);
|
||||
|
||||
this.editing = false;
|
||||
|
||||
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
|
||||
},
|
||||
onCancelTextChange() {
|
||||
this.updateValue(undefined);
|
||||
|
||||
this.editing = false;
|
||||
|
||||
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
|
||||
},
|
||||
onIncrement(direction: "Decrease" | "Increase") {
|
||||
if (this.value === undefined) return;
|
||||
|
||||
const actions = {
|
||||
Add: (): void => {
|
||||
const directionAddend = direction === "Increase" ? this.step : -this.step;
|
||||
this.updateValue(this.value !== undefined ? this.value + directionAddend : undefined);
|
||||
},
|
||||
Multiply: (): void => {
|
||||
const directionMultiplier = direction === "Increase" ? this.step : 1 / this.step;
|
||||
this.updateValue(this.value !== undefined ? this.value * directionMultiplier : undefined);
|
||||
},
|
||||
Callback: (): void => {
|
||||
if (direction === "Increase") this.incrementCallbackIncrease?.();
|
||||
if (direction === "Decrease") this.incrementCallbackDecrease?.();
|
||||
},
|
||||
None: (): void => undefined,
|
||||
};
|
||||
const action = actions[this.incrementBehavior];
|
||||
action();
|
||||
},
|
||||
updateValue(newValue: number | undefined) {
|
||||
const nowValid = this.value !== undefined && this.isInteger ? Math.round(this.value) : this.value;
|
||||
let cleaned = newValue !== undefined ? newValue : nowValid;
|
||||
|
||||
if (typeof this.min === "number" && !Number.isNaN(this.min) && cleaned !== undefined) cleaned = Math.max(cleaned, this.min);
|
||||
if (typeof this.max === "number" && !Number.isNaN(this.max) && cleaned !== undefined) cleaned = Math.min(cleaned, this.max);
|
||||
|
||||
// Required as the call to update:value can, not change the value
|
||||
this.text = this.displayText(this.value);
|
||||
|
||||
if (newValue !== undefined) this.$emit("update:value", cleaned);
|
||||
},
|
||||
displayText(value: number | undefined): string {
|
||||
if (value === undefined) return "-";
|
||||
|
||||
// Find the amount of digits on the left side of the decimal
|
||||
// 10.25 == 2
|
||||
// 1.23 == 1
|
||||
// 0.23 == 0 (Reason for the slightly more complicated code)
|
||||
const absValueInt = Math.floor(Math.abs(value));
|
||||
const leftSideDigits = absValueInt === 0 ? 0 : absValueInt.toString().length;
|
||||
const roundingPower = 10 ** Math.max(this.displayDecimalPlaces - leftSideDigits, 0);
|
||||
|
||||
const displayValue = Math.round(value * roundingPower) / roundingPower;
|
||||
|
||||
return `${displayValue}${unPluralize(this.unit, value)}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Called only when `value` is changed from outside this component (with v-model)
|
||||
value(newValue: number | undefined) {
|
||||
// Draw a dash if the value is undefined
|
||||
if (newValue === undefined) {
|
||||
this.text = "-";
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the range slider with the new value
|
||||
this.rangeSliderValue = newValue;
|
||||
this.rangeSliderValueAsRendered = newValue;
|
||||
|
||||
// The simple `clamp()` function can't be used here since `undefined` values need to be boundless
|
||||
let sanitized = newValue;
|
||||
if (typeof this.min === "number") sanitized = Math.max(sanitized, this.min);
|
||||
if (typeof this.max === "number") sanitized = Math.min(sanitized, this.max);
|
||||
|
||||
this.text = this.displayText(sanitized);
|
||||
},
|
||||
},
|
||||
components: { FieldInput },
|
||||
});
|
||||
|
||||
function unPluralize(unit: string, value: number): string {
|
||||
if (value === 1 && unit.endsWith("s")) return unit.slice(0, -1);
|
||||
return unit;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FieldInput
|
||||
class="number-input"
|
||||
@@ -240,240 +477,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { type NumberInputMode, type NumberInputIncrementBehavior } from "@/wasm-communication/messages";
|
||||
|
||||
import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:value"],
|
||||
props: {
|
||||
// Label
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
|
||||
// Disabled
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
|
||||
// Value
|
||||
value: { type: Number as PropType<number>, required: false }, // When not provided, a dash is displayed
|
||||
min: { type: Number as PropType<number>, required: false },
|
||||
max: { type: Number as PropType<number>, required: false },
|
||||
isInteger: { type: Boolean as PropType<boolean>, default: false },
|
||||
|
||||
// Number presentation
|
||||
displayDecimalPlaces: { type: Number as PropType<number>, default: 3 },
|
||||
unit: { type: String as PropType<string>, default: "" },
|
||||
unitIsHiddenWhenEditing: { type: Boolean as PropType<boolean>, default: true },
|
||||
|
||||
// Mode behavior
|
||||
// "Increment" shows arrows and allows dragging left/right to change the value.
|
||||
// "Range" shows a range slider between some minimum and maximum value.
|
||||
mode: { type: String as PropType<NumberInputMode>, default: "Increment" },
|
||||
// When `mode` is "Increment", `step` is the multiplier or addend used with `incrementBehavior`.
|
||||
// When `mode` is "Range", `step` is the range slider's snapping increment if `isInteger` is `true`.
|
||||
step: { type: Number as PropType<number>, default: 1 },
|
||||
// `incrementBehavior` is only applicable with a `mode` of "Increment".
|
||||
// "Add"/"Multiply": The value is added or multiplied by `step`.
|
||||
// "None": the increment arrows are not shown.
|
||||
// "Callback": the functions `incrementCallbackIncrease` and `incrementCallbackDecrease` call custom behavior.
|
||||
incrementBehavior: { type: String as PropType<NumberInputIncrementBehavior>, default: "Add" },
|
||||
// `rangeMin` and `rangeMax` are only applicable with a `mode` of "Range".
|
||||
// They set the lower and upper values of the slider to drag between.
|
||||
rangeMin: { type: Number as PropType<number>, default: 0 },
|
||||
rangeMax: { type: Number as PropType<number>, default: 1 },
|
||||
|
||||
// Styling
|
||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
|
||||
// Callbacks
|
||||
incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false },
|
||||
incrementCallbackDecrease: { type: Function as PropType<() => void>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
text: this.displayText(this.value),
|
||||
editing: false,
|
||||
// Stays in sync with a binding to the actual input range slider element.
|
||||
rangeSliderValue: this.value !== undefined ? this.value : 0,
|
||||
// Value used to render the position of the fake slider when applicable, and length of the progress colored region to the slider's left.
|
||||
// This is the same as `rangeSliderValue` except in the "mousedown" state, when it has the previous location before the user's mousedown.
|
||||
rangeSliderValueAsRendered: this.value !== undefined ? this.value : 0,
|
||||
// "default": no interaction is happening.
|
||||
// "mousedown": the user has pressed down the mouse and might next decide to either drag left/right or release without dragging.
|
||||
// "dragging": the user is dragging the slider left/right.
|
||||
rangeSliderClickDragState: "default" as "default" | "mousedown" | "dragging",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sliderStepValue() {
|
||||
const step = this.step === undefined ? 1 : this.step;
|
||||
return this.isInteger ? step : "any";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sliderInput() {
|
||||
// Keep only 4 digits after the decimal point
|
||||
const ROUNDING_EXPONENT = 4;
|
||||
const ROUNDING_MAGNITUDE = 10 ** ROUNDING_EXPONENT;
|
||||
const roundedValue = Math.round(this.rangeSliderValue * ROUNDING_MAGNITUDE) / ROUNDING_MAGNITUDE;
|
||||
|
||||
// Exit if this is an extraneous event invocation that occurred after mouseup, which happens in Firefox
|
||||
if (this.value !== undefined && Math.abs(this.value - roundedValue) < 1 / ROUNDING_MAGNITUDE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The first event upon mousedown means we transition to a "mousedown" state
|
||||
if (this.rangeSliderClickDragState === "default") {
|
||||
this.rangeSliderClickDragState = "mousedown";
|
||||
|
||||
// Exit early because we don't want to use the value set by where on the track the user pressed
|
||||
return;
|
||||
}
|
||||
|
||||
// The second event upon mousedown that occurs by moving left or right means the user has committed to dragging the slider
|
||||
if (this.rangeSliderClickDragState === "mousedown") {
|
||||
this.rangeSliderClickDragState = "dragging";
|
||||
}
|
||||
|
||||
// If we're in a dragging state, we want to use the new slider value
|
||||
this.rangeSliderValueAsRendered = roundedValue;
|
||||
this.updateValue(roundedValue);
|
||||
},
|
||||
sliderPointerDown() {
|
||||
// We want to render the fake slider thumb at the old position, which is still the number held by `value`
|
||||
this.rangeSliderValueAsRendered = this.value || 0;
|
||||
|
||||
// Because an `input` event is fired right before or after this (depending on browser), that first
|
||||
// invocation will transition the state machine to `mousedown`. That's why we don't do it here.
|
||||
},
|
||||
sliderPointerUp() {
|
||||
// User clicked but didn't drag, so we focus the text input element
|
||||
if (this.rangeSliderClickDragState === "mousedown") {
|
||||
const fieldInput = this.$refs.fieldInput as typeof FieldInput | undefined;
|
||||
const inputElement = fieldInput?.$el.querySelector("[data-input-element]") as HTMLInputElement | undefined;
|
||||
if (!inputElement) return;
|
||||
|
||||
// Set the slider position back to the original position to undo the user moving it
|
||||
this.rangeSliderValue = this.rangeSliderValueAsRendered;
|
||||
|
||||
// Begin editing the number text field
|
||||
inputElement.focus();
|
||||
}
|
||||
|
||||
// Releasing the mouse means we can reset the state machine
|
||||
this.rangeSliderClickDragState = "default";
|
||||
},
|
||||
onTextFocused() {
|
||||
if (this.value === undefined) this.text = "";
|
||||
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
|
||||
else this.text = `${this.value}${unPluralize(this.unit, this.value)}`;
|
||||
|
||||
this.editing = true;
|
||||
|
||||
(this.$refs.fieldInput as typeof FieldInput | undefined)?.selectAllText(this.text);
|
||||
},
|
||||
// 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 unfocused (with the `blur` event binding)
|
||||
onTextChanged() {
|
||||
// The `unFocus()` call at the bottom of this function and in `onCancelTextChange()` causes this function to be run again, so this check skips a second run
|
||||
if (!this.editing) return;
|
||||
|
||||
const parsed = parseFloat(this.text);
|
||||
const newValue = Number.isNaN(parsed) ? undefined : parsed;
|
||||
|
||||
this.updateValue(newValue);
|
||||
|
||||
this.editing = false;
|
||||
|
||||
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
|
||||
},
|
||||
onCancelTextChange() {
|
||||
this.updateValue(undefined);
|
||||
|
||||
this.editing = false;
|
||||
|
||||
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
|
||||
},
|
||||
onIncrement(direction: "Decrease" | "Increase") {
|
||||
if (this.value === undefined) return;
|
||||
|
||||
const actions = {
|
||||
Add: (): void => {
|
||||
const directionAddend = direction === "Increase" ? this.step : -this.step;
|
||||
this.updateValue(this.value !== undefined ? this.value + directionAddend : undefined);
|
||||
},
|
||||
Multiply: (): void => {
|
||||
const directionMultiplier = direction === "Increase" ? this.step : 1 / this.step;
|
||||
this.updateValue(this.value !== undefined ? this.value * directionMultiplier : undefined);
|
||||
},
|
||||
Callback: (): void => {
|
||||
if (direction === "Increase") this.incrementCallbackIncrease?.();
|
||||
if (direction === "Decrease") this.incrementCallbackDecrease?.();
|
||||
},
|
||||
None: (): void => undefined,
|
||||
};
|
||||
const action = actions[this.incrementBehavior];
|
||||
action();
|
||||
},
|
||||
updateValue(newValue: number | undefined) {
|
||||
const nowValid = this.value !== undefined && this.isInteger ? Math.round(this.value) : this.value;
|
||||
let cleaned = newValue !== undefined ? newValue : nowValid;
|
||||
|
||||
if (typeof this.min === "number" && !Number.isNaN(this.min) && cleaned !== undefined) cleaned = Math.max(cleaned, this.min);
|
||||
if (typeof this.max === "number" && !Number.isNaN(this.max) && cleaned !== undefined) cleaned = Math.min(cleaned, this.max);
|
||||
|
||||
// Required as the call to update:value can, not change the value
|
||||
this.text = this.displayText(this.value);
|
||||
|
||||
if (newValue !== undefined) this.$emit("update:value", cleaned);
|
||||
},
|
||||
displayText(value: number | undefined): string {
|
||||
if (value === undefined) return "-";
|
||||
|
||||
// Find the amount of digits on the left side of the decimal
|
||||
// 10.25 == 2
|
||||
// 1.23 == 1
|
||||
// 0.23 == 0 (Reason for the slightly more complicated code)
|
||||
const absValueInt = Math.floor(Math.abs(value));
|
||||
const leftSideDigits = absValueInt === 0 ? 0 : absValueInt.toString().length;
|
||||
const roundingPower = 10 ** Math.max(this.displayDecimalPlaces - leftSideDigits, 0);
|
||||
|
||||
const displayValue = Math.round(value * roundingPower) / roundingPower;
|
||||
|
||||
return `${displayValue}${unPluralize(this.unit, value)}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Called only when `value` is changed from outside this component (with v-model)
|
||||
value(newValue: number | undefined) {
|
||||
// Draw a dash if the value is undefined
|
||||
if (newValue === undefined) {
|
||||
this.text = "-";
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the range slider with the new value
|
||||
this.rangeSliderValue = newValue;
|
||||
this.rangeSliderValueAsRendered = newValue;
|
||||
|
||||
// The simple `clamp()` function can't be used here since `undefined` values need to be boundless
|
||||
let sanitized = newValue;
|
||||
if (typeof this.min === "number") sanitized = Math.max(sanitized, this.min);
|
||||
if (typeof this.max === "number") sanitized = Math.min(sanitized, this.max);
|
||||
|
||||
this.text = this.displayText(sanitized);
|
||||
},
|
||||
},
|
||||
components: { FieldInput },
|
||||
});
|
||||
|
||||
function unPluralize(unit: string, value: number): string {
|
||||
if (value === 1 && unit.endsWith("s")) return unit.slice(0, -1);
|
||||
return unit;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { type IconName } from "@/utility-functions/icons";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:checked"],
|
||||
props: {
|
||||
checked: { type: Boolean as PropType<boolean>, required: true },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
icon: { type: String as PropType<IconName>, default: "Checkmark" },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
components: {
|
||||
CheckboxInput,
|
||||
LayoutRow,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutRow class="optional-input" :class="disabled">
|
||||
<CheckboxInput :checked="checked" :disabled="disabled" @input="(e: Event) => $emit('update:checked', (e.target as HTMLInputElement).checked)" :icon="icon" :tooltip="tooltip" />
|
||||
@@ -24,26 +47,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { type IconName } from "@/utility-functions/icons";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:checked"],
|
||||
props: {
|
||||
checked: { type: Boolean as PropType<boolean>, required: true },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
icon: { type: String as PropType<IconName>, default: "Checkmark" },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
components: {
|
||||
CheckboxInput,
|
||||
LayoutRow,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { type RadioEntries, type RadioEntryData } from "@/wasm-communication/messages";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:selectedIndex"],
|
||||
props: {
|
||||
entries: { type: Array as PropType<RadioEntries>, required: true },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
selectedIndex: { type: Number as PropType<number>, required: true },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
methods: {
|
||||
handleEntryClick(radioEntryData: RadioEntryData) {
|
||||
const index = this.entries.indexOf(radioEntryData);
|
||||
this.$emit("update:selectedIndex", index);
|
||||
|
||||
radioEntryData.action?.();
|
||||
},
|
||||
},
|
||||
components: {
|
||||
IconLabel,
|
||||
LayoutRow,
|
||||
TextLabel,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutRow class="radio-input" :class="{ disabled }">
|
||||
<button
|
||||
@@ -88,36 +121,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { type RadioEntries, type RadioEntryData } from "@/wasm-communication/messages";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
export default defineComponent({
|
||||
emits: ["update:selectedIndex"],
|
||||
props: {
|
||||
entries: { type: Array as PropType<RadioEntries>, required: true },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
selectedIndex: { type: Number as PropType<number>, required: true },
|
||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
methods: {
|
||||
handleEntryClick(radioEntryData: RadioEntryData) {
|
||||
const index = this.entries.indexOf(radioEntryData);
|
||||
this.$emit("update:selectedIndex", index);
|
||||
|
||||
radioEntryData.action?.();
|
||||
},
|
||||
},
|
||||
components: {
|
||||
IconLabel,
|
||||
LayoutRow,
|
||||
TextLabel,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { type Color } from "@/wasm-communication/messages";
|
||||
|
||||
import ColorPicker from "@/components/floating-menus/ColorPicker.vue";
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
|
||||
export default defineComponent({
|
||||
inject: ["editor"],
|
||||
props: {
|
||||
primary: { type: Object as PropType<Color>, required: true },
|
||||
secondary: { type: Object as PropType<Color>, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
primaryOpen: false,
|
||||
secondaryOpen: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
clickPrimarySwatch() {
|
||||
this.primaryOpen = true;
|
||||
this.secondaryOpen = false;
|
||||
},
|
||||
clickSecondarySwatch() {
|
||||
this.primaryOpen = false;
|
||||
this.secondaryOpen = true;
|
||||
},
|
||||
primaryColorChanged(color: Color) {
|
||||
this.editor.instance.updatePrimaryColor(color.red, color.green, color.blue, color.alpha);
|
||||
},
|
||||
secondaryColorChanged(color: Color) {
|
||||
this.editor.instance.updateSecondaryColor(color.red, color.green, color.blue, color.alpha);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ColorPicker,
|
||||
LayoutCol,
|
||||
LayoutRow,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutCol class="swatch-pair">
|
||||
<LayoutRow class="primary swatch">
|
||||
@@ -49,48 +94,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { type Color } from "@/wasm-communication/messages";
|
||||
|
||||
import ColorPicker from "@/components/floating-menus/ColorPicker.vue";
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
|
||||
export default defineComponent({
|
||||
inject: ["editor"],
|
||||
props: {
|
||||
primary: { type: Object as PropType<Color>, required: true },
|
||||
secondary: { type: Object as PropType<Color>, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
primaryOpen: false,
|
||||
secondaryOpen: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
clickPrimarySwatch() {
|
||||
this.primaryOpen = true;
|
||||
this.secondaryOpen = false;
|
||||
},
|
||||
clickSecondarySwatch() {
|
||||
this.primaryOpen = false;
|
||||
this.secondaryOpen = true;
|
||||
},
|
||||
primaryColorChanged(color: Color) {
|
||||
this.editor.instance.updatePrimaryColor(color.red, color.green, color.blue, color.alpha);
|
||||
},
|
||||
secondaryColorChanged(color: Color) {
|
||||
this.editor.instance.updateSecondaryColor(color.red, color.green, color.blue, color.alpha);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ColorPicker,
|
||||
LayoutCol,
|
||||
LayoutRow,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
<template>
|
||||
<FieldInput
|
||||
:textarea="true"
|
||||
class="text-area-input"
|
||||
:class="{ 'has-label': label }"
|
||||
:label="label"
|
||||
:spellcheck="true"
|
||||
:disabled="disabled"
|
||||
:tooltip="tooltip"
|
||||
v-model:value="inputValue"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
ref="fieldInput"
|
||||
></FieldInput>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
@@ -74,3 +55,22 @@ export default defineComponent({
|
||||
components: { FieldInput },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FieldInput
|
||||
:textarea="true"
|
||||
class="text-area-input"
|
||||
:class="{ 'has-label': label }"
|
||||
:label="label"
|
||||
:spellcheck="true"
|
||||
:disabled="disabled"
|
||||
:tooltip="tooltip"
|
||||
v-model:value="inputValue"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
ref="fieldInput"
|
||||
></FieldInput>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -1,36 +1,3 @@
|
||||
<template>
|
||||
<FieldInput
|
||||
class="text-input"
|
||||
:class="{ centered }"
|
||||
v-model:value="text"
|
||||
:label="label"
|
||||
:spellcheck="true"
|
||||
:disabled="disabled"
|
||||
:tooltip="tooltip"
|
||||
:placeholder="placeholder"
|
||||
:style="{ 'min-width': minWidth > 0 ? `${minWidth}px` : undefined }"
|
||||
:sharpRightCorners="sharpRightCorners"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
ref="fieldInput"
|
||||
></FieldInput>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.text-input {
|
||||
input {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.centered {
|
||||
input:not(:focus) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
@@ -101,3 +68,36 @@ export default defineComponent({
|
||||
components: { FieldInput },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FieldInput
|
||||
class="text-input"
|
||||
:class="{ centered }"
|
||||
v-model:value="text"
|
||||
:label="label"
|
||||
:spellcheck="true"
|
||||
:disabled="disabled"
|
||||
:tooltip="tooltip"
|
||||
:placeholder="placeholder"
|
||||
:style="{ 'min-width': minWidth > 0 ? `${minWidth}px` : undefined }"
|
||||
:sharpRightCorners="sharpRightCorners"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
ref="fieldInput"
|
||||
></FieldInput>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.text-input {
|
||||
input {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.centered {
|
||||
input:not(:focus) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user