increase frontend flexibility
This commit is contained in:
113
frontend/src/components/CopyableDynamicSpan.vue
Normal file
113
frontend/src/components/CopyableDynamicSpan.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
value: string;
|
||||
}>();
|
||||
|
||||
const overlay = computed(() => {
|
||||
return '0'.repeat(props.value.length);
|
||||
});
|
||||
|
||||
const span = useTemplateRef<HTMLSpanElement>('data-span');
|
||||
|
||||
function copyHandler(event: ClipboardEvent) {
|
||||
const selection = document.getSelection();
|
||||
const span_value: HTMLSpanElement | null = span.value;
|
||||
if (selection && span_value && selection.containsNode(span_value, true)) {
|
||||
let copy_result = '';
|
||||
for (let i = 0; i < selection.rangeCount; i++) {
|
||||
const node_iter = document.createNodeIterator(
|
||||
selection.getRangeAt(i).commonAncestorContainer,
|
||||
NodeFilter.SHOW_ALL,
|
||||
);
|
||||
let found_start = false;
|
||||
while (true) {
|
||||
const node = node_iter.nextNode();
|
||||
if (node) {
|
||||
if (node == selection?.getRangeAt(i).startContainer) {
|
||||
found_start = true;
|
||||
}
|
||||
if (found_start && node.nodeType == Node.TEXT_NODE) {
|
||||
let append_to_copy = node.textContent?.trim() || '';
|
||||
const parent = node.parentElement;
|
||||
if (parent) {
|
||||
const copy_value =
|
||||
parent.getAttribute('copy-value');
|
||||
if (copy_value) {
|
||||
append_to_copy = copy_value;
|
||||
}
|
||||
}
|
||||
if (node == selection?.getRangeAt(i).endContainer) {
|
||||
append_to_copy = append_to_copy.substring(
|
||||
0,
|
||||
selection?.getRangeAt(i).endOffset,
|
||||
);
|
||||
}
|
||||
if (node == selection?.getRangeAt(i).startContainer) {
|
||||
append_to_copy = append_to_copy.substring(
|
||||
selection?.getRangeAt(i).startOffset,
|
||||
);
|
||||
}
|
||||
if (copy_result.length > 0) {
|
||||
copy_result += ' ';
|
||||
}
|
||||
copy_result += append_to_copy;
|
||||
}
|
||||
if (node == selection?.getRangeAt(i).endContainer) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
event.clipboardData?.setData('text/plain', copy_result);
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.body.addEventListener('copy', copyHandler);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener('copy', copyHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="test monospace" :data-after-content="value">
|
||||
<span ref="data-span" class="transparent" :copy-value="value">
|
||||
{{ overlay }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.monospace {
|
||||
font-family: variables.$monospace-text-font;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
color: transparent;
|
||||
border: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.test {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.test::after {
|
||||
content: attr(data-after-content);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue';
|
||||
import CopyableDynamicSpan from '@/components/CopyableDynamicSpan.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
value: number;
|
||||
max_width: number;
|
||||
copyable?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update', value: string): void;
|
||||
}>();
|
||||
|
||||
const copyable = computed(() => {
|
||||
return props.copyable || false;
|
||||
});
|
||||
|
||||
const display_value = computed(() => {
|
||||
if (props.value == 0) {
|
||||
return '0';
|
||||
@@ -61,7 +67,12 @@ watch([display_value], ([display_str]) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
{{ display_value }}
|
||||
<template v-if="copyable">
|
||||
<CopyableDynamicSpan :value="display_value"></CopyableDynamicSpan>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ display_value }}
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
66
frontend/src/components/TelemetryValue.vue
Normal file
66
frontend/src/components/TelemetryValue.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { useTelemetry } from '@/composables/telemetry.ts';
|
||||
import { computed, inject, type ShallowRef } from 'vue';
|
||||
import {
|
||||
WEBSOCKET_SYMBOL,
|
||||
type WebsocketHandle,
|
||||
} from '@/composables/websocket.ts';
|
||||
import NumericText from '@/components/NumericText.vue';
|
||||
|
||||
const max_update_rate = 50; // ms
|
||||
const default_update_rate = 200; // ms
|
||||
|
||||
const props = defineProps<{
|
||||
data: string;
|
||||
max_update_period?: number;
|
||||
}>();
|
||||
|
||||
const max_update_period = computed(() => {
|
||||
return Math.min(
|
||||
props.max_update_period || default_update_rate,
|
||||
max_update_rate,
|
||||
);
|
||||
});
|
||||
|
||||
const { data: telemetry_data } = useTelemetry(() => props.data);
|
||||
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
|
||||
const value = websocket.value.listen_to_telemetry(
|
||||
telemetry_data,
|
||||
max_update_period,
|
||||
true,
|
||||
);
|
||||
|
||||
const is_data_present = computed(() => {
|
||||
return value.value != null;
|
||||
});
|
||||
|
||||
const numeric_data = computed(() => {
|
||||
const val = value.value;
|
||||
if (val) {
|
||||
const type = telemetry_data.value!.data_type;
|
||||
const item_val = val.value[type];
|
||||
if (typeof item_val == 'number') {
|
||||
return item_val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="!is_data_present"> No Data </span>
|
||||
<template v-else>
|
||||
<NumericText
|
||||
v-if="numeric_data"
|
||||
:value="numeric_data"
|
||||
:max_width="10"
|
||||
></NumericText>
|
||||
<span v-else>
|
||||
Cannot Display Data of Type {{ telemetry_data!.data_type }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
</style>
|
||||
19
frontend/src/components/layout/GridLayout.vue
Normal file
19
frontend/src/components/layout/GridLayout.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
cols: number;
|
||||
equal_col_width?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="`grid`"
|
||||
:style="`grid-template-columns: repeat(${cols}, ${equal_col_width ? '1fr' : 'auto'});`"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
</style>
|
||||
33
frontend/src/components/layout/LinearLayout.vue
Normal file
33
frontend/src/components/layout/LinearLayout.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { Direction } from '@/composables/Direction.ts';
|
||||
import { Alignment } from '@/composables/Alignment.ts';
|
||||
import { Justification } from '@/composables/Justification.ts';
|
||||
import { computed } from 'vue';
|
||||
import { GapSize } from '@/composables/GapSize.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
direction: Direction;
|
||||
stretch?: boolean;
|
||||
// Direction
|
||||
justify?: Justification;
|
||||
// Cross Direction
|
||||
align?: Alignment;
|
||||
gap?: GapSize;
|
||||
}>();
|
||||
|
||||
const justification = computed(() => props.justify || Justification.Start);
|
||||
const alignment = computed(() => props.align || Alignment.Start);
|
||||
const gap_size = computed(() => props.gap || GapSize.Normal);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="`${direction} linear ${stretch ? 'stretch' : ''} ${justification} ${alignment} ${gap_size}`"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
</style>
|
||||
30
frontend/src/components/layout/ScreenLayout.vue
Normal file
30
frontend/src/components/layout/ScreenLayout.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScreenType } from '@/composables/ScreenType.ts';
|
||||
|
||||
defineProps<{
|
||||
// Whether this should be limited to the height of the viewport
|
||||
// This allows scroll bars to be pushed to inner components
|
||||
limit?: boolean;
|
||||
type: ScreenType;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="`column stretch screen ${limit ? 'limited' : ''} content ${type}`"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.screen {
|
||||
min-height: 100vh;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.screen.limited {
|
||||
max-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user