Add fullscreen / zoom to fit support

This commit is contained in:
rijkvanzanten
2021-06-11 23:58:28 -04:00
parent 180d9fa942
commit a982e7e2ed
5 changed files with 218 additions and 102 deletions

View File

@@ -58,7 +58,7 @@
"@vue/cli-plugin-vuex": "^4.5.8",
"@vue/cli-service": "^4.5.13",
"@vue/compiler-sfc": "^3.0.5",
"apexcharts": "^3.26.3",
"apexcharts": "^3.26.3",
"axios": "^0.21.1",
"base-64": "^1.0.0",
"codemirror": "^5.61.1",

View File

@@ -0,0 +1,150 @@
<template>
<div class="workspace" :class="{ editing: editMode }" :style="[workspaceSize, { transform: `scale(${zoomScale})` }]">
<insights-panel
v-for="panel in panels"
:key="panel.id"
:panel="panel"
:edit-mode="editMode"
@update="stagePanelEdits($event, panel.id)"
@delete="confirmDeletePanel = panel.id"
@duplicate="duplicatePanel(panel)"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed, inject, ref } from 'vue';
import { Panel } from '@/types';
import InsightsPanel from '../components/panel.vue';
import { useElementSize } from '@/composables/use-element-size';
export default defineComponent({
name: 'insights-workspace',
components: { InsightsPanel },
props: {
panels: {
type: Array as PropType<Panel[]>,
required: true,
},
editMode: {
type: Boolean,
default: false,
},
zoomToFit: {
type: Boolean,
default: false,
},
},
setup(props) {
const mainElement = inject('main-element', ref<Element>());
const mainElementSize = useElementSize(mainElement);
const workspaceSize = computed(() => {
const furthestPanelX = props.panels.reduce(
(aggr, panel) => {
if (panel.position_x! > aggr.position_x!) {
aggr.position_x = panel.position_x!;
aggr.width = panel.width!;
}
return aggr;
},
{ position_x: 0, width: 0 }
);
const furthestPanelY = props.panels.reduce(
(aggr, panel) => {
if (panel.position_y! > aggr.position_y!) {
aggr.position_y = panel.position_y!;
aggr.height = panel.height!;
}
return aggr;
},
{ position_y: 0, height: 0 }
);
let contentPaddingPx = 32;
if (document.querySelector('#main-content')) {
const contentPadding = getComputedStyle(
document.querySelector('#main-content') as HTMLElement
).getPropertyValue('--content-padding');
contentPaddingPx = Number(contentPadding.substring(0, contentPadding.length - 2));
}
if (props.editMode === true) {
return {
width: (furthestPanelX.position_x! + furthestPanelX.width! + 25) * 20 + 'px',
height: (furthestPanelY.position_y! + furthestPanelY.height! + 25) * 20 + 'px',
};
}
return {
width: (furthestPanelX.position_x! + furthestPanelX.width! - 1) * 20 + contentPaddingPx + 'px',
height: (furthestPanelY.position_y! + furthestPanelY.height! - 1) * 20 + contentPaddingPx + 'px',
};
});
const zoomScale = computed(() => {
if (props.zoomToFit === false) return 1;
const { width } = mainElementSize;
const contentPadding = getComputedStyle(document.querySelector('#main-content') as HTMLElement).getPropertyValue(
'--content-padding'
);
const contentPaddingPx = Number(contentPadding.substring(0, contentPadding.length - 2));
const scaleWidth: number =
width.value /
(Number(workspaceSize.value.width.substring(0, workspaceSize.value.width.length - 2)) + 2 * contentPaddingPx);
return scaleWidth;
});
return { workspaceSize, mainElement, zoomScale };
},
});
</script>
<style scoped>
.workspace {
display: grid;
grid-template-rows: repeat(auto-fill, 20px);
grid-template-columns: repeat(auto-fill, 20px);
min-width: calc(100% - var(--content-padding) - var(--content-padding));
min-height: calc(100% - 120px);
margin-left: var(--content-padding);
overflow: visible;
transform: scale(1);
transform-origin: top left;
transition: transform var(--slow) var(--transition);
}
.workspace > * {
z-index: 2;
}
.workspace::before {
position: absolute;
top: -4px;
left: -4px;
display: block;
width: calc(100% + 8px);
height: calc(100% + 8px);
background-image: radial-gradient(var(--border-normal) 10%, transparent 10%);
background-position: -6px -6px;
background-size: 20px 20px;
opacity: 0;
transition: opacity var(--slow) var(--transition);
content: '';
pointer-events: none;
}
.workspace.editing::before {
opacity: 1;
}
</style>

View File

@@ -34,9 +34,14 @@
</template>
<template v-else>
<v-button class="fullscreen" rounded icon outlined>
<v-button :active="zoomToFit" class="zoom-to-fit" rounded icon outlined @click="toggleZoomToFit">
<v-icon name="aspect_ratio" />
</v-button>
<v-button :active="fullScreen" class="fullscreen" rounded icon outlined @click="toggleFullScreen">
<v-icon name="fullscreen" />
</v-button>
<v-button rounded icon outlined @click="editMode = !editMode">
<v-icon name="edit" />
</v-button>
@@ -47,17 +52,7 @@
<insights-navigation />
</template>
<div class="workspace" :class="{ editing: editMode }" :style="workspaceSize">
<insights-panel
v-for="panel in panels"
:key="panel.id"
:panel="panel"
:edit-mode="editMode"
@update="stagePanelEdits($event, panel.id)"
@delete="confirmDeletePanel = panel.id"
@duplicate="duplicatePanel(panel)"
/>
</div>
<insights-workspace :edit-mode="editMode" :panels="panels" :zoom-to-fit="zoomToFit" />
<router-view
name="detail"
@@ -86,8 +81,8 @@
<script lang="ts">
import InsightsNavigation from '../components/navigation.vue';
import { defineComponent, computed, ref } from 'vue';
import { useInsightsStore } from '@/stores';
import { defineComponent, computed, ref, toRefs, inject } from 'vue';
import { useInsightsStore, useAppStore } from '@/stores';
import InsightsNotFound from './not-found.vue';
import { Panel } from '@/types';
import { nanoid } from 'nanoid';
@@ -95,13 +90,13 @@ import { merge, omit } from 'lodash';
import { router } from '@/router';
import { unexpectedError } from '@/utils/unexpected-error';
import api from '@/api';
import InsightsPanel from '../components/panel.vue';
import { useI18n } from 'vue-i18n';
import { pointOnLine } from '@/utils/point-on-line';
import InsightsWorkspace from '../components/workspace.vue';
export default defineComponent({
name: 'InsightsDashboard',
components: { InsightsNotFound, InsightsNavigation, InsightsPanel },
components: { InsightsNotFound, InsightsNavigation, InsightsWorkspace },
props: {
primaryKey: {
type: String,
@@ -115,11 +110,17 @@ export default defineComponent({
setup(props) {
const { t } = useI18n();
const insightsStore = useInsightsStore();
const appStore = useAppStore();
const { fullScreen } = toRefs(appStore);
const editMode = ref(false);
const confirmDeletePanel = ref<string | null>(null);
const deletingPanel = ref(false);
const saving = ref(false);
const insightsStore = useInsightsStore();
const zoomToFit = ref(false);
const currentDashboard = computed(() =>
insightsStore.dashboards.find((dashboard) => dashboard.id === props.primaryKey)
@@ -188,44 +189,6 @@ export default defineComponent({
return withBorderRadii;
});
const workspaceSize = computed(() => {
const furthestPanelX = panels.value.reduce(
(aggr, panel) => {
if (panel.position_x! > aggr.position_x!) {
aggr.position_x = panel.position_x!;
aggr.width = panel.width!;
}
return aggr;
},
{ position_x: 0, width: 0 }
);
const furthestPanelY = panels.value.reduce(
(aggr, panel) => {
if (panel.position_y! > aggr.position_y!) {
aggr.position_y = panel.position_y!;
aggr.height = panel.height!;
}
return aggr;
},
{ position_y: 0, height: 0 }
);
if (editMode.value === true) {
return {
width: (furthestPanelX.position_x! + furthestPanelX.width! + 25) * 20 + 'px',
height: (furthestPanelY.position_y! + furthestPanelY.height! + 25) * 20 + 'px',
};
}
return {
width: (furthestPanelX.position_x! + furthestPanelX.width! - 1) * 20 + 'px',
height: (furthestPanelY.position_y! + furthestPanelY.height! - 1) * 20 + 'px',
};
});
return {
currentDashboard,
editMode,
@@ -235,13 +198,16 @@ export default defineComponent({
saving,
saveChanges,
stageConfiguration,
workspaceSize,
deletingPanel,
deletePanel,
confirmDeletePanel,
cancelChanges,
duplicatePanel,
t,
toggleFullScreen,
zoomToFit,
fullScreen,
toggleZoomToFit,
};
function stagePanelEdits(edits: Partial<Panel>, key: string = props.panelKey) {
@@ -339,48 +305,21 @@ export default defineComponent({
newPanel.position_y = newPanel.position_y + 2;
stagePanelEdits(newPanel, '+');
}
function toggleFullScreen() {
fullScreen.value = !fullScreen.value;
}
function toggleZoomToFit() {
zoomToFit.value = !zoomToFit.value;
}
},
});
</script>
<style scoped>
.workspace {
position: relative;
display: grid;
grid-template-rows: repeat(auto-fill, 20px);
grid-template-columns: repeat(auto-fill, 20px);
min-width: calc(100% - var(--content-padding) - var(--content-padding));
min-height: calc(100% - 120px);
margin-right: var(--content-padding);
margin-left: var(--content-padding);
overflow: visible;
}
.workspace > * {
z-index: 2;
}
.workspace::before {
position: absolute;
top: -4px;
left: -4px;
display: block;
width: calc(100% + 8px);
height: calc(100% + 8px);
background-image: radial-gradient(var(--border-normal) 10%, transparent 10%);
background-position: -6px -6px;
background-size: 20px 20px;
opacity: 0;
transition: opacity var(--slow) var(--transition);
content: '';
pointer-events: none;
}
.workspace.editing::before {
opacity: 1;
}
.fullscreen,
.zoom-to-fit,
.clear-changes {
--v-button-color: var(--foreground-normal);
--v-button-color-hover: var(--foreground-normal);

View File

@@ -4,6 +4,7 @@ export const useAppStore = defineStore({
id: 'appStore',
state: () => ({
sidebarOpen: false,
fullScreen: false,
hydrated: false,
hydrating: false,
error: null,

View File

@@ -7,8 +7,8 @@
</template>
</v-info>
<div v-else class="private-view" :class="{ theme }">
<aside role="navigation" aria-label="Module Navigation" class="navigation" :class="{ 'is-open': navOpen }">
<div v-else class="private-view" :class="{ theme, 'full-screen': fullScreen }">
<aside role="navigation" aria-label="Module Navigation" id="navigation" :class="{ 'is-open': navOpen }">
<module-bar />
<div class="module-nav alt-colors">
<project-info />
@@ -18,7 +18,7 @@
</div>
</div>
</aside>
<div class="content" ref="contentEl">
<div id="main-content" ref="contentEl">
<header-bar
show-sidebar-toggle
:title="title"
@@ -36,7 +36,8 @@
</div>
<aside
role="contentinfo"
class="sidebar alt-colors"
id="sidebar"
class="alt-colors"
aria-label="Module Sidebar"
:class="{ 'is-open': sidebarOpen }"
@click="openSidebar"
@@ -108,7 +109,7 @@ export default defineComponent({
const notificationsPreviewActive = ref(false);
const { sidebarOpen } = toRefs(appStore);
const { sidebarOpen, fullScreen } = toRefs(appStore);
const theme = computed(() => {
return userStore.currentUser?.theme || 'auto';
@@ -118,11 +119,22 @@ export default defineComponent({
router.afterEach(async () => {
contentEl.value?.scrollTo({ top: 0 });
fullScreen.value = false;
});
useTitle(title);
return { t, navOpen, contentEl, theme, sidebarOpen, openSidebar, notificationsPreviewActive, appAccess };
return {
t,
navOpen,
contentEl,
theme,
sidebarOpen,
openSidebar,
notificationsPreviewActive,
appAccess,
fullScreen,
};
function openSidebar(event: PointerEvent) {
if (event.target && (event.target as HTMLElement).classList.contains('close') === false) {
@@ -159,7 +171,7 @@ export default defineComponent({
}
}
.navigation {
#navigation {
position: fixed;
top: 0;
left: 0;
@@ -197,7 +209,7 @@ export default defineComponent({
}
}
.content {
#main-content {
--border-radius: 6px;
--input-height: 60px;
--input-padding: 16px; // (60 - 4 - 24) / 2
@@ -228,7 +240,7 @@ export default defineComponent({
}
}
.sidebar {
#sidebar {
position: fixed;
top: 0;
right: 0;
@@ -277,5 +289,19 @@ export default defineComponent({
--content-padding: 32px;
--content-padding-bottom: 132px;
}
&.full-screen {
#navigation {
position: fixed;
transform: translateX(-100%);
transition: transform var(--slow) var(--transition);
}
#sidebar {
position: fixed;
transform: translateX(100%);
transition: transform var(--slow) var(--transition);
}
}
}
</style>