mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-04 13:15:24 +00:00
created base layout, reuseable context and logging off
This commit is contained in:
parent
9b19908914
commit
2139f3d9da
16 changed files with 554 additions and 10 deletions
web-frontend
assets/scss
components
config
directives
layouts
pages/app
plugins
store
utils
|
@ -64,3 +64,10 @@ $z-index-layout-col-3: 1;
|
|||
$z-index-layout-col-3-1: 5;
|
||||
$z-index-layout-col-3-2: 4;
|
||||
$z-index-modal: 6;
|
||||
|
||||
// The z-index of the context must always be the highest because they can open in a
|
||||
// modal.
|
||||
$z-index-context: 7;
|
||||
|
||||
// normalize overrides
|
||||
$base-font-family: $text-font-stack;
|
||||
|
|
|
@ -11,6 +11,10 @@ body {
|
|||
background-color: $color-neutral-100;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.visibility-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.context {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
z-index: $z-index-context;
|
||||
white-space: nowrap;
|
||||
background-color: $white;
|
||||
border-radius: 6px;
|
||||
|
|
158
web-frontend/components/Context.vue
Normal file
158
web-frontend/components/Context.vue
Normal file
|
@ -0,0 +1,158 @@
|
|||
<template>
|
||||
<div
|
||||
v-move-to-body
|
||||
v-click-outside="hide"
|
||||
class="context"
|
||||
:class="{ 'visibility-hidden': !open }"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isElement } from '@/utils/dom'
|
||||
|
||||
export default {
|
||||
name: 'Context',
|
||||
data() {
|
||||
return {
|
||||
open: false,
|
||||
opener: null,
|
||||
children: []
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Because we don't want the parent context to close when a user clicks 'outside' that
|
||||
* element and in the child element we need to register the child with their parent to
|
||||
* prevent this.
|
||||
*/
|
||||
beforeMount() {
|
||||
let $parent = this.$parent
|
||||
while ($parent !== undefined) {
|
||||
if ($parent.registerContextChild) {
|
||||
$parent.registerContextChild(this.$el)
|
||||
break
|
||||
}
|
||||
$parent = $parent.$parent
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Toggles the open state of the context menu.
|
||||
*
|
||||
* @param target The original element element that changed the state of the
|
||||
* context, this will be used to calculate the correct position.
|
||||
* @param vertical Bottom positions the context under the target.
|
||||
* Top positions the context above the target.
|
||||
* @param horizontal Left aligns the context with the left side of the target.
|
||||
* Right aligns the context with the right side of the target.
|
||||
* @param offset The distance between the target element and the context.
|
||||
* @param value True if context must be shown, false if not and undefine
|
||||
* will invert the current state.
|
||||
*/
|
||||
toggle(
|
||||
target,
|
||||
vertical = 'bottom',
|
||||
horizontal = 'left',
|
||||
offset = 10,
|
||||
value
|
||||
) {
|
||||
if (value === undefined) {
|
||||
value = !this.open
|
||||
}
|
||||
|
||||
if (value) {
|
||||
const css = this.calculatePosition(target, vertical, horizontal, offset)
|
||||
|
||||
// Set the calculated positions of the context.
|
||||
for (const key in css) {
|
||||
const value = css[key] !== null ? Math.ceil(css[key]) + 'px' : 'auto'
|
||||
this.$el.style[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// If we store the element who opened the context menu we can exclude the element
|
||||
// when clicked outside of this element.
|
||||
this.opener = value ? target : null
|
||||
this.open = value
|
||||
},
|
||||
hide(event) {
|
||||
// Checks if the click is inside one of our children. In that code the context
|
||||
// must stay open.
|
||||
const isChild = this.children.some(element =>
|
||||
isElement(element, event.target)
|
||||
)
|
||||
|
||||
// Checks if the context is already opened, if the click was not on the opener
|
||||
// because he can trigger the toggle method and if the click was not in one of
|
||||
// our child contexts.
|
||||
if (this.open && !isElement(this.opener, event.target) && !isChild) {
|
||||
this.open = false
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Calculates the absolute position of the context based on the original clicked
|
||||
* element.
|
||||
*/
|
||||
calculatePosition(target, vertical, horizontal, offset) {
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
const contextRect = this.$el.getBoundingClientRect()
|
||||
const positions = { top: null, right: null, bottom: null, left: null }
|
||||
|
||||
// Calculate if top, bottom, left and right positions are possible.
|
||||
const canTop = targetRect.top - contextRect.height - offset > 0
|
||||
const canBottom =
|
||||
window.innerHeight - targetRect.bottom - contextRect.height - offset > 0
|
||||
const canRight = targetRect.right - contextRect.width > 0
|
||||
const canLeft =
|
||||
window.innerWidth - targetRect.left - contextRect.width > 0
|
||||
|
||||
// If bottom, top, left or right doesn't fit, but their opposite does we switch to
|
||||
// that.
|
||||
if (vertical === 'bottom' && !canBottom && canTop) {
|
||||
vertical = 'top'
|
||||
}
|
||||
|
||||
if (vertical === 'top' && !canTop) {
|
||||
vertical = 'bottom'
|
||||
}
|
||||
|
||||
if (horizontal === 'left' && !canLeft && canRight) {
|
||||
horizontal = 'right'
|
||||
}
|
||||
|
||||
if (horizontal === 'right' && !canRight) {
|
||||
horizontal = 'left'
|
||||
}
|
||||
|
||||
// Calculate the correct positions for horizontal and vertical values.
|
||||
if (horizontal === 'left') {
|
||||
positions.left = targetRect.left
|
||||
}
|
||||
|
||||
if (horizontal === 'right') {
|
||||
positions.right = window.innerWidth - targetRect.right
|
||||
}
|
||||
|
||||
if (vertical === 'bottom') {
|
||||
positions.top = targetRect.bottom + offset
|
||||
}
|
||||
|
||||
if (vertical === 'top') {
|
||||
positions.bottom = window.innerHeight - targetRect.top + offset
|
||||
}
|
||||
|
||||
return positions
|
||||
},
|
||||
/**
|
||||
* A child context can register itself with the parent to prevent closing of the
|
||||
* parent when clicked inside the child.
|
||||
*
|
||||
* @param element HTMLElement
|
||||
*/
|
||||
registerContextChild(element) {
|
||||
this.children.push(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -25,7 +25,11 @@ export default {
|
|||
/*
|
||||
** Plugins to load before mounting the App
|
||||
*/
|
||||
plugins: [{ src: '@/plugins/auth.js' }, { src: '@/plugins/vuelidate.js' }],
|
||||
plugins: [
|
||||
{ src: '@/plugins/auth.js' },
|
||||
{ src: '@/plugins/directives.js' },
|
||||
{ src: '@/plugins/vuelidate.js' }
|
||||
],
|
||||
|
||||
/*
|
||||
** Nuxt.js modules
|
||||
|
|
19
web-frontend/directives/clickOutside.js
Normal file
19
web-frontend/directives/clickOutside.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { isElement } from '@/utils/dom'
|
||||
|
||||
/**
|
||||
* This directive calls a custom method if the user clicks outside of the
|
||||
* element.
|
||||
*/
|
||||
export default {
|
||||
bind: (el, binding, vnode) => {
|
||||
el.clickOutsideEvent = event => {
|
||||
if (!isElement(el, event.target)) {
|
||||
vnode.context[binding.expression](event)
|
||||
}
|
||||
}
|
||||
document.body.addEventListener('click', el.clickOutsideEvent)
|
||||
},
|
||||
unbind: el => {
|
||||
document.body.removeEventListener('click', el.clickOutsideEvent)
|
||||
}
|
||||
}
|
18
web-frontend/directives/moveToBody.js
Normal file
18
web-frontend/directives/moveToBody.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* This directive moves the whole element to the document body so that it can be
|
||||
* positioned over another element.
|
||||
*/
|
||||
export default {
|
||||
inserted: el => {
|
||||
const body = document.body
|
||||
|
||||
// The element is added as first child in the body so that child contexts
|
||||
// are being shown on top of their parent.
|
||||
body.insertBefore(el, body.firstChild)
|
||||
},
|
||||
unbind: el => {
|
||||
if (el.parentNode) {
|
||||
el.parentNode.removeChild(el)
|
||||
}
|
||||
}
|
||||
}
|
278
web-frontend/layouts/app.vue
Normal file
278
web-frontend/layouts/app.vue
Normal file
|
@ -0,0 +1,278 @@
|
|||
<template>
|
||||
<div :class="{ 'layout-collapsed': isCollapsed }" class="layout">
|
||||
<div class="layout-col-1 menu">
|
||||
<ul class="menu-items">
|
||||
<li class="menu-item">
|
||||
<nuxt-link :to="{ name: 'app' }" class="menu-link">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
<span class="menu-link-text">Dashboard</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="#" class="menu-link" data-context=".select">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
<span class="menu-link-text">Groups</span>
|
||||
</a>
|
||||
<div class="select hidden">
|
||||
<div class="select-search">
|
||||
<i class="select-search-icon fas fa-search"></i>
|
||||
<input
|
||||
type="text"
|
||||
class="select-search-input"
|
||||
placeholder="Search views"
|
||||
/>
|
||||
</div>
|
||||
<ul class="select-items">
|
||||
<li class="select-item active">
|
||||
<a href="#" class="select-item-link">Group name 1</a>
|
||||
<a href="#" class="select-item-options" data-context=".context">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
<div class="context hidden">
|
||||
<ul class="context-menu">
|
||||
<li>
|
||||
<a href="#">
|
||||
<i class="context-menu-icon fas fa-fw fa-pen"></i>
|
||||
Rename group
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
<i class="context-menu-icon fas fa-fw fa-trash"></i>
|
||||
Delete group
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="select-item">
|
||||
<a href="#" class="select-item-link">Group name 2</a>
|
||||
<a href="#" class="select-item-options">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="select-item">
|
||||
<a href="#" class="select-item-link">Group name 3</a>
|
||||
<a href="#" class="select-item-options">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="select-item">
|
||||
<a href="#" class="select-item-link">Group name 4</a>
|
||||
<a href="#" class="select-item-options">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="select-footer">
|
||||
<a href="#" class="select-footer-button">
|
||||
<i class="fas fa-plus"></i>
|
||||
Do something
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="menu-items">
|
||||
<li class="menu-item layout-uncollapse">
|
||||
<a class="menu-link" @click="toggleCollapsed()">
|
||||
<i class="menu-item-icon fas fa-angle-double-right"></i>
|
||||
<span class="menu-link-text">Uncollapse</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a
|
||||
class="menu-link menu-user-item"
|
||||
@click="$refs.userContext.toggle($event.target)"
|
||||
>
|
||||
{{ nameAbbreviation }}
|
||||
<span class="menu-link-text">{{ name }}</span>
|
||||
</a>
|
||||
<Context ref="userContext">
|
||||
<div class="context-menu-title">{{ name }}</div>
|
||||
<ul class="context-menu">
|
||||
<li>
|
||||
<a @click="logoff()">
|
||||
<i class="context-menu-icon fas fa-fw fa-sign-out-alt"></i>
|
||||
Logoff
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Context>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="layout-col-2 sidebar">
|
||||
<div class="sidebar-content-wrapper">
|
||||
<nav class="sidebar-content">
|
||||
<div class="sidebar-title">
|
||||
<img src="@/static/img/logo.svg" alt="" />
|
||||
</div>
|
||||
<div class="sidebar-group-title">Group name 1</div>
|
||||
<ul class="tree">
|
||||
<li class="tree-item">
|
||||
<div class="tree-action">
|
||||
<a href="#" class="tree-link">
|
||||
<i class="tree-type fas fa-database"></i>
|
||||
Vehicles
|
||||
</a>
|
||||
<a href="#" class="tree-options" data-context=".context">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
<div class="context hidden">
|
||||
<div class="context-menu-title">Vehicles</div>
|
||||
<ul class="context-menu">
|
||||
<li>
|
||||
<a href="#">
|
||||
<i class="context-menu-icon fas fa-fw fa-pen"></i>
|
||||
Rename database
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
<i class="context-menu-icon fas fa-fw fa-trash"></i>
|
||||
Delete table
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="tree-item">
|
||||
<div class="tree-action">
|
||||
<a href="#" class="tree-link">
|
||||
<i class="tree-type fas fa-angle-right"></i>
|
||||
Map nummer 1
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="tree-item active">
|
||||
<div class="tree-action">
|
||||
<a href="#" class="tree-link">
|
||||
<i class="tree-type fas fa-database"></i>
|
||||
Webshop
|
||||
</a>
|
||||
<a href="#" class="tree-options">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</div>
|
||||
<ul class="tree-subs">
|
||||
<li class="tree-sub active">
|
||||
<a href="#" class="tree-sub-link">Customers</a>
|
||||
<a href="#" class="tree-options">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tree-sub">
|
||||
<a href="#" class="tree-sub-link">Products very long name</a>
|
||||
<a href="#" class="tree-options">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tree-sub">
|
||||
<a href="#" class="tree-sub-link">Categories</a>
|
||||
<a href="#" class="tree-options">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="tree-item">
|
||||
<div class="tree-action">
|
||||
<a href="#" class="tree-link">
|
||||
<i class="tree-type fas fa-angle-down"></i>
|
||||
Map nummer 1
|
||||
</a>
|
||||
</div>
|
||||
<ul class="tree">
|
||||
<li class="tree-item">
|
||||
<div class="tree-action">
|
||||
<a href="#" class="tree-link">
|
||||
<i class="tree-type fas fa-database"></i>
|
||||
Vehicles
|
||||
</a>
|
||||
<a href="#" class="tree-options">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="tree-item">
|
||||
<div class="tree-action">
|
||||
<a href="#" class="tree-link">
|
||||
<i class="tree-type fas fa-database"></i>
|
||||
Something
|
||||
</a>
|
||||
<a href="#" class="tree-options">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="tree-item">
|
||||
<div class="tree-action">
|
||||
<a href="#" class="tree-link">
|
||||
<i class="tree-type fas fa-database"></i>
|
||||
Vehicles with very long name and that is not good.
|
||||
</a>
|
||||
<a href="#" class="tree-options">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="tree-item">
|
||||
<div class="tree-action">
|
||||
<a href="#" class="tree-link">
|
||||
<i class="tree-type fas fa-database"></i>
|
||||
Something else
|
||||
</a>
|
||||
<a href="#" class="tree-options">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="sidebar-footer">
|
||||
<a class="sidebar-collapse" @click="toggleCollapsed()">
|
||||
<i class="fas fa-angle-double-left"></i>
|
||||
Collapse sidebar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-col-3">
|
||||
<nuxt />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
import Context from '@/components/Context'
|
||||
|
||||
export default {
|
||||
layout: 'default',
|
||||
middleware: 'authenticated',
|
||||
components: {
|
||||
Context
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isCollapsed: 'sidebar/isCollapsed',
|
||||
name: 'auth/getName',
|
||||
nameAbbreviation: 'auth/getNameAbbreviation'
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
logoff() {
|
||||
this.$store.dispatch('auth/logoff')
|
||||
this.$nuxt.$router.replace({ name: 'login' })
|
||||
},
|
||||
...mapActions({
|
||||
toggleCollapsed: 'sidebar/toggleCollapsed'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<nuxt />
|
||||
</div>
|
||||
</template>
|
|
@ -8,7 +8,7 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
middleware: 'authenticated',
|
||||
layout: 'app',
|
||||
computed: {
|
||||
...mapState({
|
||||
user: state => state.auth.user
|
||||
|
|
7
web-frontend/plugins/directives.js
Normal file
7
web-frontend/plugins/directives.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
import moveToBody from '@/directives/moveToBody'
|
||||
import clickOutside from '@/directives/clickOutside'
|
||||
|
||||
Vue.directive('move-to-body', moveToBody)
|
||||
Vue.directive('click-outside', clickOutside)
|
|
@ -49,6 +49,14 @@ export const actions = {
|
|||
}
|
||||
)
|
||||
},
|
||||
/**
|
||||
* Logs off the user by removing the token as a cookie and clearing the user
|
||||
* data.
|
||||
*/
|
||||
logoff({ commit }) {
|
||||
unsetToken(this.app.$cookies)
|
||||
commit('CLEAR_USER_DATA')
|
||||
},
|
||||
/**
|
||||
* Refresh the existing token. If successful commit the new token and start a
|
||||
* new refresh timeout. If unsuccessful the existing cookie and user data is
|
||||
|
@ -100,10 +108,19 @@ export const getters = {
|
|||
token(state) {
|
||||
return state.token
|
||||
},
|
||||
getName(state) {
|
||||
return state.user ? state.user.first_name : ''
|
||||
},
|
||||
getNameAbbreviation(state) {
|
||||
return state.user ? state.user.first_name.split('')[0] : ''
|
||||
},
|
||||
getEmail(state) {
|
||||
return state.user ? state.user.email : ''
|
||||
},
|
||||
/**
|
||||
* Returns the amount of seconds it will take before the tokes expires.
|
||||
* @TODO figure out what happens if the browser and server time and not very
|
||||
* much in sync.
|
||||
* @TODO figure out what happens if the browser and server time are not in
|
||||
* sync.
|
||||
*/
|
||||
tokenExpireSeconds(state) {
|
||||
const now = Math.ceil(new Date().getTime() / 1000)
|
||||
|
|
24
web-frontend/store/sidebar.js
Normal file
24
web-frontend/store/sidebar.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
export const state = () => ({
|
||||
collapsed: false
|
||||
})
|
||||
|
||||
export const mutations = {
|
||||
SET_COLLAPSED(state, collapsed) {
|
||||
state.collapsed = collapsed
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
toggleCollapsed({ commit, getters }, value) {
|
||||
if (value === undefined) {
|
||||
value = !getters.isCollapsed
|
||||
}
|
||||
commit('SET_COLLAPSED', value)
|
||||
}
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
isCollapsed(state) {
|
||||
return !!state.collapsed
|
||||
}
|
||||
}
|
9
web-frontend/utils/dom.js
Normal file
9
web-frontend/utils/dom.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Checks if the target is the same as the provided element of that the element
|
||||
* contains the target. Returns true is this is the case.
|
||||
*
|
||||
* @returns boolean
|
||||
*/
|
||||
export const isElement = (element, target) => {
|
||||
return element === target || element.contains(target)
|
||||
}
|
Loading…
Add table
Reference in a new issue