This commit is contained in:
Alexander Drozdov
2021-01-08 23:57:09 +02:00
parent faaef043fa
commit fa04d7cfec
7 changed files with 235 additions and 163 deletions
+2 -2
View File
@@ -4,8 +4,8 @@
<div style="top: 0; left: 0; height: 100%; width: 100%; position: absolute;"
:style="{ background: overlayBackground }"
@click="handleBackgroundClick"></div>
<template v-for="widget of widgets">
<component :key="widget.wmId"
<template v-for="widget of widgets" :key="widget.wmId">
<component
v-show="isVisible(widget.wmId)"
:config="widget"
:id="`widget-${widget.wmId}`"
+121 -97
View File
@@ -28,19 +28,41 @@
<div v-if="isHandleShown('cc')" :class="$style.mover" @mousedown="startMove('cc', $event)" style="left: calc(50% - 0.5rem); top: calc(50% - 0.5rem);"></div>
</div>
</div>
<div v-if="isMoving" :class="[$style.mover, $style.active]" :style="moverPosition" @mousedown="startMove(anchor.pos, $event)"></div>
<div v-if="isMoving" :class="[$style.mover, $style.active]" :style="moverPosition" @mousedown="startMove(config.anchor.pos, $event)"></div>
</div>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent, PropType, computed, inject, ref } from 'vue'
import { Widget, Anchor, WidgetManager } from './interfaces'
function useRemovable (remove: () => void) {
const isRemoving = ref(false)
let tmid: ReturnType<typeof setTimeout> | null = null
function startRemoveTimer () {
isRemoving.value = true
tmid = setTimeout(remove, 1000)
}
function cancelRemoveTimer () {
isRemoving.value = false
if (tmid !== null) {
clearTimeout(tmid)
tmid = null
}
}
return { startRemoveTimer, cancelRemoveTimer, isRemoving }
}
export default defineComponent({
props: {
config: {
type: Object,
type: Object as PropType<Widget & { anchor: Anchor }>,
required: true
},
moveHandles: {
type: [String, Array],
type: [String, Array] as PropType<string | string[]>,
default: undefined
},
readonly: {
@@ -56,148 +78,150 @@ export default {
default: true
}
},
inject: ['wm'],
data () {
return {
isEditing: false,
isMoving: false,
isRemoving: false
}
},
computed: {
anchor () {
return this.config.anchor
},
moverPosition () {
setup (props) {
const wm = inject<WidgetManager>('wm')!
const moverPosition = computed(() => {
const { anchor, wmZorder } = props.config
return {
'top': `max(0%, min(calc(${this.anchor.y}% - (1rem/2)), calc(100% - 1rem)))`,
'left': `max(0%, min(calc(${this.anchor.x}% - (1rem/2)), calc(100% - 1rem)))`,
'z-index': this.config.wmZorder
'top': `max(0%, min(calc(${anchor.y}% - (1rem/2)), calc(100% - 1rem)))`,
'left': `max(0%, min(calc(${anchor.x}% - (1rem/2)), calc(100% - 1rem)))`,
'z-index': wmZorder
}
},
widgetPosition () {
let translate
})
const widgetPosition = computed(() => {
const { anchor, wmZorder } = props.config
// <top, center, bottom><left, center, right>
if (this.anchor.pos === 'tl') {
let translate
if (anchor.pos === 'tl') {
translate = undefined
} else if (this.anchor.pos === 'tc') {
} else if (anchor.pos === 'tc') {
translate = 'translate(-50%, 0%)'
} else if (this.anchor.pos === 'tr') {
} else if (anchor.pos === 'tr') {
translate = 'translate(-100%, 0%)'
} else if (this.anchor.pos === 'cr') {
} else if (anchor.pos === 'cr') {
translate = 'translate(-100%, -50%)'
} else if (this.anchor.pos === 'br') {
} else if (anchor.pos === 'br') {
translate = 'translate(-100%, -100%)'
} else if (this.anchor.pos === 'bc') {
} else if (anchor.pos === 'bc') {
translate = 'translate(-50%, -100%)'
} else if (this.anchor.pos === 'bl') {
} else if (anchor.pos === 'bl') {
translate = 'translate(0%, -100%)'
} else if (this.anchor.pos === 'cl') {
} else if (anchor.pos === 'cl') {
translate = 'translate(0%, -50%)'
} else if (this.anchor.pos === 'cc') {
} else if (anchor.pos === 'cc') {
translate = 'translate(-50%, -50%)'
}
return {
'top': `${this.anchor.y}%`,
'left': `${this.anchor.x}%`,
'top': `${anchor.y}%`,
'left': `${anchor.x}%`,
'transform': translate,
'z-index': this.config.wmZorder
'z-index': wmZorder
}
},
actionsPosition () {
if (this.anchor.x <= 50 && this.anchor.y <= 50) {
})
const actionsPosition = computed(() => {
const { anchor } = props.config
if (anchor.x <= 50 && anchor.y <= 50) {
return {
top: '0',
left: '100%'
}
}
if (this.anchor.x >= 50 && this.anchor.y <= 50) {
if (anchor.x >= 50 && anchor.y <= 50) {
return {
top: '0',
right: '100%'
}
}
if (this.anchor.x >= 50 && this.anchor.y >= 50) {
if (anchor.x >= 50 && anchor.y >= 50) {
return {
bottom: '0',
right: '100%'
}
}
if (this.anchor.x <= 50 && this.anchor.y >= 50) {
if (anchor.x <= 50 && anchor.y >= 50) {
return {
bottom: '0',
left: '100%'
}
}
return null
},
shownHandles () {
if (!this.moveHandles) {
})
const shownHandles = computed(() => {
if (!props.moveHandles) {
return ['tl', 'tc', 'tr', 'cr', 'br', 'bc', 'bl', 'cl', 'cc']
}
if (this.moveHandles === 'center') {
if (props.moveHandles === 'center') {
return ['cc']
}
if (this.moveHandles === 'corners') {
if (props.moveHandles === 'corners') {
return ['tl', 'tr', 'br', 'bl']
}
if (this.moveHandles === 'top-bottom') {
if (props.moveHandles === 'top-bottom') {
return ['tl', 'tc', 'tr', 'br', 'bc', 'bl']
}
return []
}
},
methods: {
startMove (pos, e) {
this.anchor.pos = pos
this.updatePosition(e)
document.addEventListener('mousemove', this.updatePosition)
document.addEventListener('mouseup', this.endMove)
},
endMove () {
document.removeEventListener('mousemove', this.updatePosition)
document.removeEventListener('mouseup', this.endMove)
},
updatePosition (e) {
this.anchor.x = Math.min(Math.max(e.clientX / window.innerWidth, 0), 1) * 100
this.anchor.y = Math.min(Math.max(e.clientY / window.innerHeight, 0), 1) * 100
},
isHandleShown (pos) {
return this.anchor.pos !== pos &&
this.shownHandles.includes(pos)
},
toggleEdit () {
this.isEditing = !this.isEditing
this.isMoving = false
},
toggleMove () {
this.isMoving = !this.isMoving
this.isEditing = false
},
hide () {
this.wm.hide(this.config.wmId)
},
remove () {
this.wm.remove(this.config.wmId)
},
startRemoveTimer () {
this.isRemoving = true
this.removeTimeout = setTimeout(this.remove, 1000)
},
cancelRemoveTimer () {
this.isRemoving = false
clearTimeout(this.removeTimeout)
this.removeTimeout = undefined
},
handleMouseDown () {
if (this.config.wmId != null) {
this.wm.bringToTop(this.config.wmId)
})
function startMove (pos: string, e: MouseEvent) {
props.config.anchor.pos = pos
updatePosition(e)
document.addEventListener('mousemove', updatePosition)
document.addEventListener('mouseup', endMove)
function endMove () {
document.removeEventListener('mousemove', updatePosition)
document.removeEventListener('mouseup', endMove)
}
}
function updatePosition (e: MouseEvent) {
props.config.anchor.x = Math.min(Math.max(e.clientX / window.innerWidth, 0), 1) * 100
props.config.anchor.y = Math.min(Math.max(e.clientY / window.innerHeight, 0), 1) * 100
}
const isEditing = ref(false)
const isMoving = ref(false)
return {
moverPosition,
widgetPosition,
actionsPosition,
handleMouseDown () {
// @TODO: why null check?
if (props.config.wmId != null) {
wm.bringToTop(props.config.wmId)
}
},
startMove,
isMoving,
isEditing,
isHandleShown (pos: string) {
return props.config.anchor.pos !== pos &&
shownHandles.value.includes(pos)
},
hide () {
wm.hide(props.config.wmId)
},
toggleEdit () {
isEditing.value = !isEditing.value
isMoving.value = false
},
toggleMove () {
isMoving.value = !isMoving.value
isEditing.value = false
},
...useRemovable(() => {
wm.remove(props.config.wmId)
})
}
}
}
})
</script>
<style lang="postcss" module>
+34 -29
View File
@@ -2,17 +2,17 @@
<widget :config="config" :hideable="false" :removable="false" move-handles="corners" v-slot="{ isEditing }">
<div class="widget-default-style">
<div class="py-1 pr-1 flex items-center text-base">
<template v-for="widget in widgets">
<button :key="widget.wmId" @click="toggle(widget)"
<template v-for="widget in widgets" :key="widget.wmId">
<button @click="toggle(widget)"
:class="widget.wmWants === 'show' ? 'border-gray-500' : 'border-gray-900'"
class="bg-gray-800 rounded text-gray-100 ml-1 p-2 leading-none whitespace-no-wrap border">{{ widget.wmTitle || `#${widget.wmId}` }}</button>
</template>
<ui-popper :delayOnMouseOut="150">
<template slot="reference">
<ui-popover>
<template #target>
<button class="rounded text-gray-600 ml-1 px-2 py-1 leading-none"><i class="fas fa-ellipsis-h"></i></button>
</template>
<div class="popper">
<div class="flex flex-col justify-center text-base text-left">
<template #content>
<div class="flex flex-col justify-center text-base">
<!-- <button class="text-left hover:bg-gray-400 rounded px-1 whitespace-no-wrap">Chromatic calculator</button> -->
<!-- <button class="text-left hover:bg-gray-400 rounded px-1 whitespace-no-wrap">Screen saver</button> -->
<!-- add widget -->
@@ -22,8 +22,8 @@
<button class="text-left hover:bg-gray-400 rounded px-1 whitespace-no-wrap" @click="createOfType('image-strip')">{{ $t('Image strip') }}</button>
<!-- <button class="text-left hover:bg-gray-400 rounded px-1 whitespace-no-wrap" @click="createOfType('TODO')">Image</button> -->
</div>
</div>
</ui-popper>
</template>
</ui-popover>
</div>
<div v-if="isEditing" class="text-gray-100 px-2 pb-1 whitespace-no-wrap">
<ui-toggle v-model="config.alwaysShow">{{ $t('Show button for active widgets') }}</ui-toggle>
@@ -32,39 +32,44 @@
</widget>
</template>
<script>
import Widget from './Widget'
<script lang="ts">
import { defineComponent, PropType, computed, inject, ref } from 'vue'
import { Widget as IWidget, WidgetManager, WidgetMenu } from './interfaces'
import Widget from './Widget.vue'
export default {
export default defineComponent({
components: { Widget },
props: {
config: {
type: Object,
type: Object as PropType<WidgetMenu>,
required: true
}
},
inject: ['wm'],
computed: {
widgets () {
return this.wm.widgets.filter(widget =>
setup (props) {
const wm = inject<WidgetManager>('wm')!
const widgets = computed(() => {
return wm.widgets.filter(widget =>
!widget.wmFlags.includes('skip-menu') &&
(this.config.alwaysShow || (widget.wmWants === 'hide'))
(props.config.alwaysShow || (widget.wmWants === 'hide'))
)
}
},
methods: {
createOfType (type) {
this.wm.create(type)
},
toggle (widget) {
if (widget.wmWants === 'hide') {
this.wm.show(widget.wmId)
} else {
this.wm.hide(widget.wmId)
})
return {
widgets,
createOfType (type: string) {
wm.create(type)
},
toggle (widget: IWidget) {
if (widget.wmWants === 'hide') {
wm.show(widget.wmId)
} else {
wm.hide(widget.wmId)
}
}
}
}
}
})
</script>
<style lang="postcss" module>
+39
View File
@@ -0,0 +1,39 @@
export interface Widget {
wmId: number
wmType: string
wmTitle: string
wmWants: 'show' | 'hide'
wmZorder: number | 'exclusive' | null
wmFlags: Array<WellKnownFlag | string>
// ---------------
[key: string]: unknown
}
export type WellKnownFlag =
'uninitialized' |
'skip-menu' |
'has-browser' |
'invisible-on-blur' |
'hide-on-blur' |
'hide-on-blur(close)' |
'hide-on-focus'
export interface Anchor {
pos: string
x: number
y: number
}
export interface WidgetManager {
widgets: Widget[]
show (wmId: number): void
hide (wmId: number): void
remove (wmId: number): void
bringToTop (wmId: number): void
create (wmType: string): void
}
export interface WidgetMenu extends Widget {
anchor: Anchor
alwaysShow: boolean
}
+11 -14
View File
@@ -3,19 +3,18 @@
@mouseenter="isHovered = true"
@mouseleave="isHovered = false">
<img :class="$style.img" :src="isBundled ? require(`@/assets/images/${src}`) : src">
<portal v-if="isHovered && !disabled" mount-to="body" append>
<teleport v-if="isHovered && !disabled" to="body">
<div :class="$style.imgFullscreenWrapper">
<img :class="$style.imgFullscreen" :src="isBundled ? require(`@/assets/images/${src}`) : src">
</div>
</portal>
</teleport>
</div>
</template>
<script>
import { MountingPortal as Portal } from 'portal-vue'
<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
export default {
components: { Portal },
export default defineComponent({
name: 'FullscreenImage',
props: {
src: {
@@ -27,17 +26,15 @@ export default {
default: false
}
},
data () {
setup (props) {
const isHovered = ref(false)
const isBundled = computed(() => !props.src.includes('://'))
return {
isHovered: false
}
},
computed: {
isBundled () {
return !this.src.includes('://')
isHovered,
isBundled
}
}
}
})
</script>
<style lang="postcss" module>
+15 -13
View File
@@ -1,31 +1,33 @@
<template>
<input
:value="value"
:value="modelValue"
@input="update"
@mousewheel="noop">
</template>
<script>
<script lang="ts">
import { defineComponent } from 'vue'
import { debounce } from 'throttle-debounce'
export default {
export default defineComponent({
name: 'UiInputDebounced',
emits: ['update:modelValue'],
props: {
value: {},
modelValue: {},
delay: {
type: Number,
default: 350
}
},
created () {
this.update = debounce(this.delay, (e) => {
this.$emit('input', e.target.value)
})
},
methods: {
noop () {
// fix not working mouseScroll
setup (props, ctx) {
return {
update: debounce(props.delay, (e: InputEvent) => {
ctx.emit('update:modelValue', (e.target as HTMLInputElement).value)
}),
noop () {
// fix not working mouseScroll
}
}
}
}
})
</script>
+13 -8
View File
@@ -1,25 +1,30 @@
<template>
<button
@click="updateInput" class="flex items-center" style="height: 1.375rem;">
<i v-if="value" class="fas fa-toggle-on pr-1 text-gray-300"></i>
<i v-if="modelValue" class="fas fa-toggle-on pr-1 text-gray-300"></i>
<i v-else class="fas fa-toggle-off pr-1 text-gray-600"></i>
<slot />
</button>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'UiToggle',
emits: ['update:modelValue'],
props: {
value: {
modelValue: {
type: Boolean,
required: true
}
},
methods: {
updateInput () {
this.$emit('input', !this.value)
setup (props, ctx) {
return {
updateInput () {
ctx.emit('update:modelValue', !props.modelValue)
}
}
}
}
})
</script>