mirror of
https://github.com/directus/directus.git
synced 2026-02-02 03:45:07 -05:00
Add fullscreen / zoom to fit support
This commit is contained in:
@@ -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",
|
||||
|
||||
150
app/src/modules/insights/components/workspace.vue
Normal file
150
app/src/modules/insights/components/workspace.vue
Normal 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>
|
||||
@@ -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);
|
||||
|
||||
@@ -4,6 +4,7 @@ export const useAppStore = defineStore({
|
||||
id: 'appStore',
|
||||
state: () => ({
|
||||
sidebarOpen: false,
|
||||
fullScreen: false,
|
||||
hydrated: false,
|
||||
hydrating: false,
|
||||
error: null,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user