Move JS script to the top of each Vue file

This commit is contained in:
Keavon Chambers
2023-03-10 03:44:49 -08:00
parent 1c50f0030c
commit 2327524690
50 changed files with 3482 additions and 3517 deletions
@@ -1,20 +1,3 @@
<!-- TODO: Refactor this component (together with `WidgetRow.vue`) to be more logically consistent with our layout definition goals, in terms of naming and capabilities -->
<template>
<div class="widget-layout">
<component :is="layoutGroupType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layout.layoutTarget" v-for="(layoutRow, index) in layout.layout" :key="index" />
</div>
</template>
<style lang="scss">
.widget-layout {
height: 100%;
flex: 0 0 auto;
display: flex;
flex-direction: column;
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
@@ -42,3 +25,20 @@ export default defineComponent({
},
});
</script>
<!-- TODO: Refactor this component (together with `WidgetRow.vue`) to be more logically consistent with our layout definition goals, in terms of naming and capabilities -->
<template>
<div class="widget-layout">
<component :is="layoutGroupType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layout.layoutTarget" v-for="(layoutRow, index) in layout.layout" :key="index" />
</div>
</template>
<style lang="scss">
.widget-layout {
height: 100%;
flex: 0 0 auto;
display: flex;
flex-direction: column;
}
</style>
+99 -100
View File
@@ -1,3 +1,102 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { debouncer } from "@/components/widgets/debounce";
import { isWidgetColumn, isWidgetRow, type WidgetColumn, type WidgetRow, type Widget } from "@/wasm-communication/messages";
import PivotAssist from "@/components/widgets/assists/PivotAssist.vue";
import BreadcrumbTrailButtons from "@/components/widgets/buttons/BreadcrumbTrailButtons.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import ParameterExposeButton from "@/components/widgets/buttons/ParameterExposeButton.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
import FontInput from "@/components/widgets/inputs/FontInput.vue";
import LayerReferenceInput from "@/components/widgets/inputs/LayerReferenceInput.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
import SwatchPairInput from "@/components/widgets/inputs/SwatchPairInput.vue";
import TextAreaInput from "@/components/widgets/inputs/TextAreaInput.vue";
import TextInput from "@/components/widgets/inputs/TextInput.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import Separator from "@/components/widgets/labels/Separator.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
const SUFFIX_WIDGETS = ["PopoverButton"];
export default defineComponent({
inject: ["editor"],
props: {
widgetData: { type: Object as PropType<WidgetColumn | WidgetRow>, required: true },
layoutTarget: { required: true },
},
data() {
return {
open: false,
};
},
computed: {
direction(): "column" | "row" | "ERROR" {
if (isWidgetColumn(this.widgetData)) return "column";
if (isWidgetRow(this.widgetData)) return "row";
return "ERROR";
},
widgets() {
let widgets: Widget[] = [];
if (isWidgetColumn(this.widgetData)) widgets = this.widgetData.columnWidgets;
if (isWidgetRow(this.widgetData)) widgets = this.widgetData.rowWidgets;
return widgets;
},
widgetsAndNextSiblingIsSuffix(): [Widget, boolean][] {
return this.widgets.map((widget, index): [Widget, boolean] => {
// A suffix widget is one that joins up with this widget at the end with only a 1px gap.
// It uses the CSS sibling selector to give its own left edge corners zero radius.
// But this JS is needed to set its preceding sibling widget's right edge corners to zero radius.
const nextSiblingIsSuffix = SUFFIX_WIDGETS.includes(this.widgets[index + 1]?.props.kind);
return [widget, nextSiblingIsSuffix];
});
},
},
methods: {
updateLayout(index: number, value: unknown) {
this.editor.instance.updateLayout(this.layoutTarget, this.widgets[index].widgetId, value);
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
withoutValue(props: Record<string, any>): Record<string, unknown> {
const { value: _, ...rest } = props;
return rest;
},
debouncer,
},
components: {
BreadcrumbTrailButtons,
CheckboxInput,
ColorInput,
DropdownInput,
FontInput,
IconButton,
IconLabel,
LayerReferenceInput,
NumberInput,
OptionalInput,
ParameterExposeButton,
PivotAssist,
PopoverButton,
RadioInput,
Separator,
SwatchPairInput,
TextAreaInput,
TextButton,
TextInput,
TextLabel,
},
});
</script>
<!-- TODO: Refactor this component to use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
<!-- TODO: Also rename this component, and probably move the `widget-${direction}` wrapper to be part of `WidgetLayout.vue` as part of its refactor -->
@@ -112,103 +211,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { debouncer } from "@/components/widgets/debounce";
import { isWidgetColumn, isWidgetRow, type WidgetColumn, type WidgetRow, type Widget } from "@/wasm-communication/messages";
import PivotAssist from "@/components/widgets/assists/PivotAssist.vue";
import BreadcrumbTrailButtons from "@/components/widgets/buttons/BreadcrumbTrailButtons.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import ParameterExposeButton from "@/components/widgets/buttons/ParameterExposeButton.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
import FontInput from "@/components/widgets/inputs/FontInput.vue";
import LayerReferenceInput from "@/components/widgets/inputs/LayerReferenceInput.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
import SwatchPairInput from "@/components/widgets/inputs/SwatchPairInput.vue";
import TextAreaInput from "@/components/widgets/inputs/TextAreaInput.vue";
import TextInput from "@/components/widgets/inputs/TextInput.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import Separator from "@/components/widgets/labels/Separator.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
const SUFFIX_WIDGETS = ["PopoverButton"];
export default defineComponent({
inject: ["editor"],
props: {
widgetData: { type: Object as PropType<WidgetColumn | WidgetRow>, required: true },
layoutTarget: { required: true },
},
data() {
return {
open: false,
};
},
computed: {
direction(): "column" | "row" | "ERROR" {
if (isWidgetColumn(this.widgetData)) return "column";
if (isWidgetRow(this.widgetData)) return "row";
return "ERROR";
},
widgets() {
let widgets: Widget[] = [];
if (isWidgetColumn(this.widgetData)) widgets = this.widgetData.columnWidgets;
if (isWidgetRow(this.widgetData)) widgets = this.widgetData.rowWidgets;
return widgets;
},
widgetsAndNextSiblingIsSuffix(): [Widget, boolean][] {
return this.widgets.map((widget, index): [Widget, boolean] => {
// A suffix widget is one that joins up with this widget at the end with only a 1px gap.
// It uses the CSS sibling selector to give its own left edge corners zero radius.
// But this JS is needed to set its preceding sibling widget's right edge corners to zero radius.
const nextSiblingIsSuffix = SUFFIX_WIDGETS.includes(this.widgets[index + 1]?.props.kind);
return [widget, nextSiblingIsSuffix];
});
},
},
methods: {
updateLayout(index: number, value: unknown) {
this.editor.instance.updateLayout(this.layoutTarget, this.widgets[index].widgetId, value);
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
withoutValue(props: Record<string, any>): Record<string, unknown> {
const { value: _, ...rest } = props;
return rest;
},
debouncer,
},
components: {
BreadcrumbTrailButtons,
CheckboxInput,
ColorInput,
DropdownInput,
FontInput,
IconButton,
IconLabel,
LayerReferenceInput,
NumberInput,
OptionalInput,
ParameterExposeButton,
PivotAssist,
PopoverButton,
RadioInput,
Separator,
SwatchPairInput,
TextAreaInput,
TextButton,
TextInput,
TextLabel,
},
});
</script>
@@ -1,3 +1,22 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type PivotPosition } from "@/wasm-communication/messages";
export default defineComponent({
emits: ["update:position"],
props: {
position: { type: String as PropType<string>, required: true },
disabled: { type: Boolean as PropType<boolean>, default: false },
},
methods: {
setPosition(newPosition: PivotPosition) {
this.$emit("update:position", newPosition);
},
},
});
</script>
<template>
<div class="pivot-assist" :class="{ disabled }">
<button @click="setPosition('TopLeft')" class="row-1 col-1" :class="{ active: position === 'TopLeft' }" tabindex="-1" :disabled="disabled"><div></div></button>
@@ -101,21 +120,3 @@
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type PivotPosition } from "@/wasm-communication/messages";
export default defineComponent({
emits: ["update:position"],
props: {
position: { type: String as PropType<string>, required: true },
disabled: { type: Boolean as PropType<boolean>, default: false },
},
methods: {
setPosition(newPosition: PivotPosition) {
this.$emit("update:position", newPosition);
},
},
});
</script>
@@ -1,3 +1,25 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
export default defineComponent({
props: {
labels: { type: Array as PropType<string[]>, required: true },
disabled: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
// Callbacks
action: { type: Function as PropType<(index: number) => void>, required: true },
},
components: {
LayoutRow,
TextButton,
},
});
</script>
<template>
<LayoutRow class="breadcrumb-trail-buttons" :title="tooltip">
<TextButton
@@ -57,25 +79,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
export default defineComponent({
props: {
labels: { type: Array as PropType<string[]>, required: true },
disabled: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
// Callbacks
action: { type: Function as PropType<(index: number) => void>, required: true },
},
components: {
LayoutRow,
TextButton,
},
});
</script>
@@ -1,3 +1,26 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type IconName, type IconSize } from "@/utility-functions/icons";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
props: {
icon: { type: String as PropType<IconName>, required: true },
size: { type: Number as PropType<IconSize>, required: true },
disabled: { type: Boolean as PropType<boolean>, default: false },
active: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
// Callbacks
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
},
components: { IconLabel },
});
</script>
<template>
<button
class="icon-button"
@@ -78,26 +101,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type IconName, type IconSize } from "@/utility-functions/icons";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
export default defineComponent({
props: {
icon: { type: String as PropType<IconName>, required: true },
size: { type: Number as PropType<IconSize>, required: true },
disabled: { type: Boolean as PropType<boolean>, default: false },
active: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
// Callbacks
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
},
components: { IconLabel },
});
</script>
@@ -1,3 +1,21 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
export default defineComponent({
props: {
exposed: { type: Boolean as PropType<boolean>, required: true },
dataType: { type: String as PropType<string>, required: true },
tooltip: { type: String as PropType<string | undefined>, required: false },
// Callbacks
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
},
components: { LayoutRow },
});
</script>
<template>
<LayoutRow class="parameter-expose-button">
<button :class="{ exposed }" :style="{ '--data-type-color': `var(--color-data-${dataType})` }" @click="(e: MouseEvent) => action(e)" :title="tooltip" :tabindex="0"></button>
@@ -39,21 +57,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
export default defineComponent({
props: {
exposed: { type: Boolean as PropType<boolean>, required: true },
dataType: { type: String as PropType<string>, required: true },
tooltip: { type: String as PropType<string | undefined>, required: false },
// Callbacks
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
},
components: { LayoutRow },
});
</script>
@@ -1,3 +1,41 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type IconName } from "@/utility-functions/icons";
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
export default defineComponent({
props: {
icon: { type: String as PropType<IconName>, default: "DropdownArrow" },
tooltip: { type: String as PropType<string | undefined>, required: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
// Callbacks
action: { type: Function as PropType<() => void>, required: false },
},
data() {
return {
open: false,
};
},
methods: {
onClick() {
this.open = true;
this.action?.();
},
},
components: {
FloatingMenu,
IconButton,
LayoutRow,
},
});
</script>
<template>
<LayoutRow class="popover-button">
<IconButton :class="{ open }" :disabled="disabled" :action="() => onClick()" :icon="icon" :size="16" data-floating-menu-spawner :tooltip="tooltip" />
@@ -50,41 +88,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type IconName } from "@/utility-functions/icons";
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
export default defineComponent({
props: {
icon: { type: String as PropType<IconName>, default: "DropdownArrow" },
tooltip: { type: String as PropType<string | undefined>, required: false },
disabled: { type: Boolean as PropType<boolean>, default: false },
// Callbacks
action: { type: Function as PropType<() => void>, required: false },
},
data() {
return {
open: false,
};
},
methods: {
onClick() {
this.open = true;
this.action?.();
},
},
components: {
FloatingMenu,
IconButton,
LayoutRow,
},
});
</script>
@@ -1,3 +1,31 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type IconName } from "@/utility-functions/icons";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
export default defineComponent({
props: {
label: { type: String as PropType<string>, required: true },
icon: { type: String as PropType<IconName | undefined>, required: false },
emphasized: { type: Boolean as PropType<boolean>, default: false },
minWidth: { type: Number as PropType<number>, default: 0 },
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 },
// Callbacks
action: { type: Function as PropType<(e: MouseEvent) => void>, required: true },
},
components: {
IconLabel,
TextLabel,
},
});
</script>
<template>
<button
class="text-button"
@@ -69,31 +97,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type IconName } from "@/utility-functions/icons";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
export default defineComponent({
props: {
label: { type: String as PropType<string>, required: true },
icon: { type: String as PropType<IconName | undefined>, required: false },
emphasized: { type: Boolean as PropType<boolean>, default: false },
minWidth: { type: Number as PropType<number>, default: 0 },
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 },
// Callbacks
action: { type: Function as PropType<(e: MouseEvent) => void>, required: true },
},
components: {
IconLabel,
TextLabel,
},
});
</script>
@@ -1,3 +1,46 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { isWidgetRow, isWidgetSection, type LayoutGroup, type WidgetSection as WidgetSectionFromJsMessages } from "@/wasm-communication/messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
import WidgetRow from "@/components/widgets/WidgetRow.vue";
const WidgetSection = defineComponent({
name: "WidgetSection",
inject: ["editor"],
props: {
widgetData: { type: Object as PropType<WidgetSectionFromJsMessages>, required: true },
layoutTarget: { required: true },
},
data: () => ({
isWidgetRow,
isWidgetSection,
expanded: true,
}),
methods: {
updateLayout(widgetId: bigint, value: unknown) {
this.editor.instance.updateLayout(this.layoutTarget, widgetId, value);
},
layoutGroupType(layoutGroup: LayoutGroup): unknown {
if (isWidgetRow(layoutGroup)) return WidgetRow;
if (isWidgetSection(layoutGroup)) return WidgetSection;
throw new Error("Layout row type does not exist");
},
},
components: {
LayoutCol,
LayoutRow,
TextLabel,
WidgetRow,
},
});
export default WidgetSection;
</script>
<!-- TODO: Implement collapsable sections with properties system -->
<template>
<LayoutCol class="widget-section">
@@ -120,46 +163,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { isWidgetRow, isWidgetSection, type LayoutGroup, type WidgetSection as WidgetSectionFromJsMessages } from "@/wasm-communication/messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
import WidgetRow from "@/components/widgets/WidgetRow.vue";
const WidgetSection = defineComponent({
name: "WidgetSection",
inject: ["editor"],
props: {
widgetData: { type: Object as PropType<WidgetSectionFromJsMessages>, required: true },
layoutTarget: { required: true },
},
data: () => ({
isWidgetRow,
isWidgetSection,
expanded: true,
}),
methods: {
updateLayout(widgetId: bigint, value: unknown) {
this.editor.instance.updateLayout(this.layoutTarget, widgetId, value);
},
layoutGroupType(layoutGroup: LayoutGroup): unknown {
if (isWidgetRow(layoutGroup)) return WidgetRow;
if (isWidgetSection(layoutGroup)) return WidgetSection;
throw new Error("Layout row type does not exist");
},
},
components: {
LayoutCol,
LayoutRow,
TextLabel,
WidgetRow,
},
});
export default WidgetSection;
</script>
@@ -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>
@@ -1,3 +1,28 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type IconName, ICONS, ICON_COMPONENTS } from "@/utility-functions/icons";
import LayoutRow from "@/components/layout/LayoutRow.vue";
export default defineComponent({
props: {
icon: { type: String as PropType<IconName>, required: true },
disabled: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
},
computed: {
iconSizeClass(): string {
return `size-${ICONS[this.icon].size}`;
},
},
components: {
LayoutRow,
...ICON_COMPONENTS,
},
});
</script>
<template>
<LayoutRow :class="['icon-label', iconSizeClass, { disabled }]" :title="tooltip">
<component :is="icon" />
@@ -29,28 +54,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type IconName, ICONS, ICON_COMPONENTS } from "@/utility-functions/icons";
import LayoutRow from "@/components/layout/LayoutRow.vue";
export default defineComponent({
props: {
icon: { type: String as PropType<IconName>, required: true },
disabled: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
},
computed: {
iconSizeClass(): string {
return `size-${ICONS[this.icon].size}`;
},
},
components: {
LayoutRow,
...ICON_COMPONENTS,
},
});
</script>
@@ -1,3 +1,16 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type SeparatorDirection, type SeparatorType } from "@/wasm-communication/messages";
export default defineComponent({
props: {
direction: { type: String as PropType<SeparatorDirection>, default: "Horizontal" },
type: { type: String as PropType<SeparatorType>, default: "Unrelated" },
},
});
</script>
<template>
<div class="separator" :class="[direction.toLowerCase(), type.toLowerCase()]">
<div v-if="['Section', 'List'].includes(type)"></div>
@@ -71,16 +84,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { type SeparatorDirection, type SeparatorType } from "@/wasm-communication/messages";
export default defineComponent({
props: {
direction: { type: String as PropType<SeparatorDirection>, default: "Horizontal" },
type: { type: String as PropType<SeparatorType>, default: "Unrelated" },
},
});
</script>
@@ -1,3 +1,19 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
export default defineComponent({
props: {
disabled: { type: Boolean as PropType<boolean>, default: false },
bold: { type: Boolean as PropType<boolean>, default: false },
italic: { type: Boolean as PropType<boolean>, default: false },
tableAlign: { type: Boolean as PropType<boolean>, default: false },
minWidth: { type: Number as PropType<number>, default: 0 },
multiline: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
},
});
</script>
<template>
<span class="text-label" :class="{ disabled, bold, italic, multiline, 'table-align': tableAlign }" :style="{ 'min-width': minWidth > 0 ? `${minWidth}px` : undefined }" :title="tooltip">
<slot></slot>
@@ -34,19 +50,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
export default defineComponent({
props: {
disabled: { type: Boolean as PropType<boolean>, default: false },
bold: { type: Boolean as PropType<boolean>, default: false },
italic: { type: Boolean as PropType<boolean>, default: false },
tableAlign: { type: Boolean as PropType<boolean>, default: false },
minWidth: { type: Number as PropType<number>, default: 0 },
multiline: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
},
});
</script>
@@ -1,127 +1,3 @@
<template>
<IconLabel class="user-input-label keyboard-lock-notice" v-if="displayKeyboardLockNotice" :icon="'Info'" :title="keyboardLockInfoMessage" />
<LayoutRow class="user-input-label" v-else>
<template v-for="(keysWithLabels, i) in keysWithLabelsGroups" :key="i">
<Separator :type="'Related'" v-if="i > 0"></Separator>
<template v-for="(keyInfo, j) in keyTextOrIconList(keysWithLabels)" :key="j">
<div class="input-key" :class="keyInfo.width">
<IconLabel v-if="keyInfo.icon" :icon="keyInfo.icon" />
<TextLabel v-else-if="keyInfo.label !== undefined">{{ keyInfo.label }}</TextLabel>
</div>
</template>
</template>
<div class="input-mouse" v-if="mouseMotion">
<IconLabel :icon="mouseHintIcon(mouseMotion)" />
</div>
<div class="hint-text" v-if="hasSlotContent">
<slot></slot>
</div>
</LayoutRow>
</template>
<style lang="scss">
.user-input-label {
flex: 0 0 auto;
height: 100%;
align-items: center;
white-space: nowrap;
.input-key,
.input-mouse {
& + .input-key,
& + .input-mouse {
margin-left: 2px;
}
}
.input-key {
display: flex;
justify-content: center;
align-items: center;
font-family: "Inconsolata", monospace;
font-weight: 400;
text-align: center;
height: 16px;
box-sizing: border-box;
border: 1px solid;
border-radius: 4px;
border-color: var(--color-5-dullgray);
color: var(--color-e-nearwhite);
.text-label {
// Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height,
// so moving it up 1 pixel by using 15px makes them agree.
line-height: 15px;
}
&.width-1 {
width: 16px;
}
&.width-2 {
width: 24px;
}
&.width-3 {
width: 32px;
}
&.width-4 {
width: 40px;
}
&.width-5 {
width: 48px;
}
.icon-label {
margin: 1px;
}
}
.input-mouse {
.bright {
fill: var(--color-e-nearwhite);
}
.dim {
fill: var(--color-8-uppergray);
}
}
.hint-text {
margin-left: 4px;
}
.floating-menu-content .row > & {
.input-key {
border-color: var(--color-3-darkgray);
color: var(--color-8-uppergray);
}
.input-key .icon-label svg,
&.keyboard-lock-notice.keyboard-lock-notice svg,
.input-mouse .bright {
fill: var(--color-8-uppergray);
}
.input-mouse .dim {
fill: var(--color-3-darkgray);
}
}
.floating-menu-content .row:hover > & {
.input-key {
border-color: var(--color-7-middlegray);
}
.input-mouse .dim {
fill: var(--color-7-middlegray);
}
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
@@ -247,3 +123,127 @@ export default defineComponent({
},
});
</script>
<template>
<IconLabel class="user-input-label keyboard-lock-notice" v-if="displayKeyboardLockNotice" :icon="'Info'" :title="keyboardLockInfoMessage" />
<LayoutRow class="user-input-label" v-else>
<template v-for="(keysWithLabels, i) in keysWithLabelsGroups" :key="i">
<Separator :type="'Related'" v-if="i > 0"></Separator>
<template v-for="(keyInfo, j) in keyTextOrIconList(keysWithLabels)" :key="j">
<div class="input-key" :class="keyInfo.width">
<IconLabel v-if="keyInfo.icon" :icon="keyInfo.icon" />
<TextLabel v-else-if="keyInfo.label !== undefined">{{ keyInfo.label }}</TextLabel>
</div>
</template>
</template>
<div class="input-mouse" v-if="mouseMotion">
<IconLabel :icon="mouseHintIcon(mouseMotion)" />
</div>
<div class="hint-text" v-if="hasSlotContent">
<slot></slot>
</div>
</LayoutRow>
</template>
<style lang="scss">
.user-input-label {
flex: 0 0 auto;
height: 100%;
align-items: center;
white-space: nowrap;
.input-key,
.input-mouse {
& + .input-key,
& + .input-mouse {
margin-left: 2px;
}
}
.input-key {
display: flex;
justify-content: center;
align-items: center;
font-family: "Inconsolata", monospace;
font-weight: 400;
text-align: center;
height: 16px;
box-sizing: border-box;
border: 1px solid;
border-radius: 4px;
border-color: var(--color-5-dullgray);
color: var(--color-e-nearwhite);
.text-label {
// Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height,
// so moving it up 1 pixel by using 15px makes them agree.
line-height: 15px;
}
&.width-1 {
width: 16px;
}
&.width-2 {
width: 24px;
}
&.width-3 {
width: 32px;
}
&.width-4 {
width: 40px;
}
&.width-5 {
width: 48px;
}
.icon-label {
margin: 1px;
}
}
.input-mouse {
.bright {
fill: var(--color-e-nearwhite);
}
.dim {
fill: var(--color-8-uppergray);
}
}
.hint-text {
margin-left: 4px;
}
.floating-menu-content .row > & {
.input-key {
border-color: var(--color-3-darkgray);
color: var(--color-8-uppergray);
}
.input-key .icon-label svg,
&.keyboard-lock-notice.keyboard-lock-notice svg,
.input-mouse .bright {
fill: var(--color-8-uppergray);
}
.input-mouse .dim {
fill: var(--color-3-darkgray);
}
}
.floating-menu-content .row:hover > & {
.input-key {
border-color: var(--color-7-middlegray);
}
.input-mouse .dim {
fill: var(--color-7-middlegray);
}
}
}
</style>
@@ -1,47 +1,3 @@
<template>
<div class="canvas-ruler" :class="direction.toLowerCase()" ref="canvasRuler">
<svg :style="svgBounds">
<path :d="svgPath" />
<text v-for="(svgText, index) in svgTexts" :key="index" :transform="svgText.transform">{{ svgText.text }}</text>
</svg>
</div>
</template>
<style lang="scss">
.canvas-ruler {
flex: 1 1 100%;
background: var(--color-4-dimgray);
overflow: hidden;
position: relative;
&.horizontal {
height: 16px;
}
&.vertical {
width: 16px;
svg text {
text-anchor: end;
}
}
svg {
position: absolute;
path {
stroke-width: 1px;
stroke: var(--color-7-middlegray);
}
text {
font-size: 12px;
fill: var(--color-8-uppergray);
}
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
@@ -146,3 +102,47 @@ export default defineComponent({
},
});
</script>
<template>
<div class="canvas-ruler" :class="direction.toLowerCase()" ref="canvasRuler">
<svg :style="svgBounds">
<path :d="svgPath" />
<text v-for="(svgText, index) in svgTexts" :key="index" :transform="svgText.transform">{{ svgText.text }}</text>
</svg>
</div>
</template>
<style lang="scss">
.canvas-ruler {
flex: 1 1 100%;
background: var(--color-4-dimgray);
overflow: hidden;
position: relative;
&.horizontal {
height: 16px;
}
&.vertical {
width: 16px;
svg text {
text-anchor: end;
}
}
svg {
position: absolute;
path {
stroke-width: 1px;
stroke: var(--color-7-middlegray);
}
text {
font-size: 12px;
fill: var(--color-8-uppergray);
}
}
}
</style>
@@ -1,111 +1,3 @@
<template>
<div class="persistent-scrollbar" :class="direction.toLowerCase()">
<button class="arrow decrease" @pointerdown="() => changePosition(-50)" tabindex="-1"></button>
<div class="scroll-track" ref="scrollTrack" @pointerdown="(e) => grabArea(e)">
<div class="scroll-thumb" @pointerdown="(e) => grabHandle(e)" :class="{ dragging }" :style="[thumbStart, thumbEnd, sides]"></div>
</div>
<button class="arrow increase" @click="() => changePosition(50)" tabindex="-1"></button>
</div>
</template>
<style lang="scss">
.persistent-scrollbar {
display: flex;
flex: 1 1 100%;
.arrow {
flex: 0 0 auto;
background: none;
border: none;
border-style: solid;
width: 0;
height: 0;
margin: 0;
padding: 0;
}
.scroll-track {
flex: 1 1 100%;
position: relative;
.scroll-thumb {
position: absolute;
border-radius: 4px;
background: var(--color-5-dullgray);
&:hover,
&.dragging {
background: var(--color-6-lowergray);
}
}
.scroll-click-area {
position: absolute;
}
}
&.vertical {
flex-direction: column;
.arrow.decrease {
margin: 4px 3px;
border-width: 0 5px 8px 5px;
border-color: transparent transparent var(--color-5-dullgray) transparent;
&:hover {
border-color: transparent transparent var(--color-6-lowergray) transparent;
}
&:active {
border-color: transparent transparent var(--color-c-brightgray) transparent;
}
}
.arrow.increase {
margin: 4px 3px;
border-width: 8px 5px 0 5px;
border-color: var(--color-5-dullgray) transparent transparent transparent;
&:hover {
border-color: var(--color-6-lowergray) transparent transparent transparent;
}
&:active {
border-color: var(--color-c-brightgray) transparent transparent transparent;
}
}
}
&.horizontal {
flex-direction: row;
.arrow.decrease {
margin: 3px 4px;
border-width: 5px 8px 5px 0;
border-color: transparent var(--color-5-dullgray) transparent transparent;
&:hover {
border-color: transparent var(--color-6-lowergray) transparent transparent;
}
&:active {
border-color: transparent var(--color-c-brightgray) transparent transparent;
}
}
.arrow.increase {
margin: 3px 4px;
border-width: 5px 0 5px 8px;
border-color: transparent transparent transparent var(--color-5-dullgray);
&:hover {
border-color: transparent transparent transparent var(--color-6-lowergray);
}
&:active {
border-color: transparent transparent transparent var(--color-c-brightgray);
}
}
}
}
</style>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
@@ -217,3 +109,111 @@ export default defineComponent({
},
});
</script>
<template>
<div class="persistent-scrollbar" :class="direction.toLowerCase()">
<button class="arrow decrease" @pointerdown="() => changePosition(-50)" tabindex="-1"></button>
<div class="scroll-track" ref="scrollTrack" @pointerdown="(e) => grabArea(e)">
<div class="scroll-thumb" @pointerdown="(e) => grabHandle(e)" :class="{ dragging }" :style="[thumbStart, thumbEnd, sides]"></div>
</div>
<button class="arrow increase" @click="() => changePosition(50)" tabindex="-1"></button>
</div>
</template>
<style lang="scss">
.persistent-scrollbar {
display: flex;
flex: 1 1 100%;
.arrow {
flex: 0 0 auto;
background: none;
border: none;
border-style: solid;
width: 0;
height: 0;
margin: 0;
padding: 0;
}
.scroll-track {
flex: 1 1 100%;
position: relative;
.scroll-thumb {
position: absolute;
border-radius: 4px;
background: var(--color-5-dullgray);
&:hover,
&.dragging {
background: var(--color-6-lowergray);
}
}
.scroll-click-area {
position: absolute;
}
}
&.vertical {
flex-direction: column;
.arrow.decrease {
margin: 4px 3px;
border-width: 0 5px 8px 5px;
border-color: transparent transparent var(--color-5-dullgray) transparent;
&:hover {
border-color: transparent transparent var(--color-6-lowergray) transparent;
}
&:active {
border-color: transparent transparent var(--color-c-brightgray) transparent;
}
}
.arrow.increase {
margin: 4px 3px;
border-width: 8px 5px 0 5px;
border-color: var(--color-5-dullgray) transparent transparent transparent;
&:hover {
border-color: var(--color-6-lowergray) transparent transparent transparent;
}
&:active {
border-color: var(--color-c-brightgray) transparent transparent transparent;
}
}
}
&.horizontal {
flex-direction: row;
.arrow.decrease {
margin: 3px 4px;
border-width: 5px 8px 5px 0;
border-color: transparent var(--color-5-dullgray) transparent transparent;
&:hover {
border-color: transparent var(--color-6-lowergray) transparent transparent;
}
&:active {
border-color: transparent var(--color-c-brightgray) transparent transparent;
}
}
.arrow.increase {
margin: 3px 4px;
border-width: 5px 0 5px 8px;
border-color: transparent transparent transparent var(--color-5-dullgray);
&:hover {
border-color: transparent transparent transparent var(--color-6-lowergray);
}
&:active {
border-color: transparent transparent transparent var(--color-c-brightgray);
}
}
}
}
</style>