New page types: tabs, maps; misc. fixes (#198)
Upgrade to Framework7 5.4.5 Add leaflet/vue2-leaflet dependencies Widgets & pages: - Add OhTabsPage, OhMapPage, OhTab, OhMapMarker, OhMapCircleMarker - Add support for tabbed pages in modals - Add theme, themeOptions, device, JSON to expression evaluation context Page designers: - Add rudimentary designers for tabs, map pages (to refactor) - Import designer components dynamically (split into individual webpack chunks) - Fix masonry menus z-index issues in editor (hopefully) Config parameters: - Add map picker for location contexts Code editor: - Add indent guides Signed-off-by: Yannick Schaus <github@schaus.net>pull/199/head
parent
bcd161e774
commit
b9f2f3ddd3
|
@ -7800,9 +7800,9 @@
|
|||
}
|
||||
},
|
||||
"framework7": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/framework7/-/framework7-5.3.2.tgz",
|
||||
"integrity": "sha512-pqsc0vVDwGOdZ7mFIPXX07tHzdtbAqEcfnw0rtURFdZuOxq9Aic65FGH1aIr2t/tTFebxkQ0XTVK0DKCVMmwBA==",
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/framework7/-/framework7-5.4.5.tgz",
|
||||
"integrity": "sha512-PkrEJAjK6bX1R43en81hGmzUiaRGgrY+W+eM4VArcEDXTwUR+goSelGZjYA4mxTLSjJVT9WIosf0VxoWkZYf8w==",
|
||||
"requires": {
|
||||
"dom7": "2.1.3",
|
||||
"path-to-regexp": "6.1.0",
|
||||
|
@ -7816,9 +7816,9 @@
|
|||
"integrity": "sha512-+mB8XhEAaIjjfRNlqW2tk7kYfPrQvDmDVpRFUjeUOiPDqqOybYmC9McvVPwwmqmob4nD3iZFWfs+rAMkVs5vPQ=="
|
||||
},
|
||||
"framework7-vue": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/framework7-vue/-/framework7-vue-5.3.2.tgz",
|
||||
"integrity": "sha512-9znW9aChVIzKS0X7T607qMUT0D8OiQc8AVQXx4Qmx/MyrNqBsDISav37aT7dKQ+WyN14VTkq8bcSWoQWkA8DXA=="
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/framework7-vue/-/framework7-vue-5.4.5.tgz",
|
||||
"integrity": "sha512-QzdSXO4srot2pCLUAdJewdq5KTUE/b7BI3zE8WLfPEroyNlFMitBc83KkJ/h35FwgmAzaNDm3eGXrwTx/OWoyQ=="
|
||||
},
|
||||
"fresh": {
|
||||
"version": "0.5.2",
|
||||
|
@ -10424,6 +10424,11 @@
|
|||
"invert-kv": "2.0.0"
|
||||
}
|
||||
},
|
||||
"leaflet": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.6.0.tgz",
|
||||
"integrity": "sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ=="
|
||||
},
|
||||
"left-pad": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
|
||||
|
@ -16645,6 +16650,11 @@
|
|||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||
"dev": true
|
||||
},
|
||||
"vue2-leaflet": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/vue2-leaflet/-/vue2-leaflet-2.5.2.tgz",
|
||||
"integrity": "sha512-9eN0TxqCkyXbaI7waO3u+n0OAezkxjb811tstG6gRLAZy/ocXlNLC3JqTWE0FwBUlqBbMpyzsIk6LrEhs8oVBQ=="
|
||||
},
|
||||
"vuex": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.2.tgz",
|
||||
|
|
|
@ -60,10 +60,11 @@
|
|||
"dom7": "^2.1.3",
|
||||
"echarts": "^4.6.0",
|
||||
"expression-eval": "^2.1.0",
|
||||
"framework7": "^5.3.2",
|
||||
"framework7": "^5.4.5",
|
||||
"framework7-icons": "^3.0.0",
|
||||
"framework7-vue": "^5.3.2",
|
||||
"framework7-vue": "^5.4.5",
|
||||
"later-again": "^0.1.1",
|
||||
"leaflet": "^1.6.0",
|
||||
"moo": "^0.5.1",
|
||||
"nearley": "^2.19.1",
|
||||
"template7": "^1.4.2",
|
||||
|
@ -73,6 +74,7 @@
|
|||
"vue-codemirror": "^4.0.6",
|
||||
"vue-echarts": "^4.1.0",
|
||||
"vue-magic-grid": "0.0.4",
|
||||
"vue2-leaflet": "^2.5.2",
|
||||
"vuex": "^3.1.2",
|
||||
"yaml": "^1.7.2"
|
||||
},
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
:class="{ currentsection: currentUrl.indexOf('/page/' + page.uid) >= 0 }"
|
||||
:link="'/page/' + page.uid"
|
||||
:title="page.config.label" view=".view-main" panel-close>
|
||||
<f7-icon slot="media" f7="tv"></f7-icon>
|
||||
<f7-icon slot="media" :f7="pageIcon(page)"></f7-icon>
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
<f7-block-title>Administration</f7-block-title>
|
||||
|
@ -285,6 +285,20 @@ export default {
|
|||
})
|
||||
})
|
||||
},
|
||||
pageIcon (page) {
|
||||
switch (page.component) {
|
||||
case 'Sitemap':
|
||||
return 'menu'
|
||||
case 'oh-layout-page':
|
||||
return 'rectangle_grid_2x2'
|
||||
case 'oh-tabs-page':
|
||||
return 'squares_below_rectangle'
|
||||
case 'oh-map-page':
|
||||
return 'map'
|
||||
default:
|
||||
return 'tv'
|
||||
}
|
||||
},
|
||||
login () {
|
||||
localStorage.setItem('openhab.ui:serverUrl', this.serverUrl)
|
||||
localStorage.setItem('openhab.ui:username', this.username)
|
||||
|
|
|
@ -10,29 +10,100 @@
|
|||
@input="updateValue"
|
||||
type="text">
|
||||
<div class="padding-left" slot="content-end">
|
||||
<f7-button slot="content-end" @click="openMap"><f7-icon f7="placemark" /> Map</f7-button>
|
||||
<f7-button slot="content-end" @click="openMapPicker"><f7-icon f7="placemark" /> Map</f7-button>
|
||||
</div>
|
||||
</f7-list-input>
|
||||
<f7-popup ref="mapPicker" class="mappicker-popup" close-on-escape :opened="mapPickerOpen" @popup:opened="mapPickerOpened" @popup:closed="mapPickerClosed">
|
||||
<f7-page>
|
||||
<f7-navbar>
|
||||
<f7-nav-left>
|
||||
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" popup-close></f7-link>
|
||||
</f7-nav-left>
|
||||
<f7-nav-title>{{configDescription.label}}</f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link @click="updateValue(marker)">Done</f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
|
||||
<l-map
|
||||
v-if="showMap"
|
||||
:zoom="zoom"
|
||||
:center="center"
|
||||
:options="mapOptions"
|
||||
@click="mapClicked"
|
||||
class="oh-map-page-lmap">
|
||||
<l-tile-layer
|
||||
:url="url"
|
||||
:attribution="attribution"
|
||||
/>
|
||||
<l-marker :lat-lng="marker" />
|
||||
</l-map>
|
||||
|
||||
</f7-page>
|
||||
|
||||
</f7-popup>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// TODO: map
|
||||
import { latLng, Icon } from 'leaflet'
|
||||
import { LMap, LTileLayer, LMarker } from 'vue2-leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
|
||||
delete Icon.Default.prototype._getIconUrl
|
||||
Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
|
||||
iconUrl: require('leaflet/dist/images/marker-icon.png'),
|
||||
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
|
||||
})
|
||||
|
||||
export default {
|
||||
props: ['configDescription', 'value'],
|
||||
components: {
|
||||
LMap,
|
||||
LTileLayer,
|
||||
LMarker
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
mapOpened: false
|
||||
mapPickerOpen: false,
|
||||
zoom: 13,
|
||||
center: latLng(52.5200066, 13.4049540),
|
||||
// url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
url: `https://a.basemaps.cartocdn.com/${this.$f7.data.themeOptions.dark}_all/{z}/{x}/{y}.png`,
|
||||
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a>, © <a href="https://carto.com/attribution/">CARTO</a>',
|
||||
marker: latLng(52.5200066, 13.4049540),
|
||||
showMap: false,
|
||||
mapOptions: {
|
||||
zoomSnap: 0.5
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValue (event) {
|
||||
if (event.lat && event.lng) {
|
||||
this.$emit('input', [event.lat, event.lng].join(','))
|
||||
} else {
|
||||
this.$emit('input', event.target.value)
|
||||
}
|
||||
this.mapPickerOpen = false
|
||||
},
|
||||
openMap () {
|
||||
this.$f7.dialog.alert('Map location picker not implemented')
|
||||
mapPickerClosed () {
|
||||
this.showMap = false
|
||||
this.mapPickerOpen = false
|
||||
},
|
||||
mapPickerOpened () {
|
||||
this.$nextTick(() => { this.showMap = true })
|
||||
},
|
||||
mapClicked (evt) {
|
||||
this.marker = latLng(evt.latlng)
|
||||
},
|
||||
openMapPicker () {
|
||||
this.mapPickerOpen = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,15 @@
|
|||
height 100%
|
||||
width 100%
|
||||
|
||||
.CodeMirror-line
|
||||
line-height 1.3
|
||||
|
||||
.cm-lkcampbell-indent-guides
|
||||
margin-top -5px
|
||||
background-repeat repeat-y
|
||||
background-image url("data:image/svg+xml;utf8,<?xml version='1.0' encoding='UTF-8'?><svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='1px' height='2px'><rect width='1' height='1' style='fill:%2377777777' /></svg>")
|
||||
position relative
|
||||
|
||||
.CodeMirror-hints
|
||||
z-index 999999
|
||||
.CodeMirror-Tern-tooltip
|
||||
|
@ -68,6 +77,42 @@ import tern from 'tern'
|
|||
import EcmascriptDefs from 'tern/defs/ecmascript.json'
|
||||
import OpenhabDefs from '@/assets/openhab-tern-defs.json'
|
||||
|
||||
// Adapted from https://github.com/lkcampbell/brackets-indent-guides (MIT)
|
||||
var indentGuidesOverlay = {
|
||||
token: function (stream, state) {
|
||||
var char = '',
|
||||
colNum = 0,
|
||||
spaceUnits = 0,
|
||||
isTabStart = false
|
||||
|
||||
char = stream.next()
|
||||
colNum = stream.column()
|
||||
|
||||
if (colNum === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (char === '\t') {
|
||||
return 'lkcampbell-indent-guides'
|
||||
}
|
||||
|
||||
if (char !== ' ') {
|
||||
stream.skipToEnd()
|
||||
return null
|
||||
}
|
||||
|
||||
spaceUnits = 2
|
||||
isTabStart = !(colNum % spaceUnits)
|
||||
|
||||
if ((char === ' ') && (isTabStart)) {
|
||||
return 'lkcampbell-indent-guides'
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
flattenSpans: false
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
codemirror
|
||||
|
@ -125,6 +170,8 @@ export default {
|
|||
}
|
||||
extraKeys['Shift-Tab'] = 'indentLess'
|
||||
cm.setOption('extraKeys', extraKeys)
|
||||
cm.addOverlay(indentGuidesOverlay)
|
||||
cm.refresh()
|
||||
},
|
||||
onCmCodeChange (newCode) {
|
||||
this.$emit('input', newCode)
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
<generic-widget-component class="magic-grid-item" :context="childContext(slotComponent)" v-for="(slotComponent, idx) in context.component.slots.default" :key="idx" v-on="$listeners" />
|
||||
</template>
|
||||
</magic-grid>
|
||||
<div v-else class="oh-columns-grid">
|
||||
<div v-for="(slotComponent, idx) in context.component.slots.default" :key="idx" class="oh-column-item">
|
||||
<f7-menu v-if="context.editmode" class="configure-layout-menu" :style="{ 'z-index': 100 - context.component.slots.default.indexOf(slotComponent) }">
|
||||
<f7-menu-item style="margin-left: auto" icon-f7="slider_horizontal_below_rectangle" dropdown>
|
||||
<div v-else class="oh-masonry">
|
||||
<div v-for="(slotComponent, idx) in context.component.slots.default" :key="idx" class="oh-masonry-item" :style="{ 'min-height': dropdownMenuOpened === idx ? 'calc(10 * var(--f7-menu-dropdown-item-height))' : undefined, 'z-index': 100 - context.component.slots.default.indexOf(slotComponent) }">
|
||||
<f7-menu v-if="context.editmode" class="configure-layout-menu">
|
||||
<f7-menu-item style="margin-left: auto" icon-f7="slider_horizontal_below_rectangle" dropdown @menu:opened="dropdownMenuOpened = idx" @menu:closed="dropdownMenuOpened = null">
|
||||
<f7-menu-dropdown right>
|
||||
<f7-menu-dropdown-item @click="context.editmode.configureWidget(slotComponent, context)" href="#" text="Configure Widget"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="context.editmode.editWidgetCode(slotComponent, context)" href="#" text="Edit YAML"></f7-menu-dropdown-item>
|
||||
|
@ -45,6 +45,11 @@ export default {
|
|||
mixins: [mixin],
|
||||
components: {
|
||||
OhPlaceholderWidget
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
dropdownMenuOpened: null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -58,16 +63,18 @@ export default {
|
|||
.magic-grid-item
|
||||
width calc(100% - 2 * var(--oh-grid-gap))
|
||||
|
||||
.oh-columns-grid
|
||||
.oh-masonry
|
||||
column-count 6
|
||||
column-width 300px
|
||||
column-gap 0
|
||||
margin-left auto
|
||||
margin-right auto
|
||||
min-height 300px
|
||||
.oh-column-item
|
||||
.oh-masonry-item
|
||||
width 100%
|
||||
display inline-block
|
||||
.list
|
||||
z-index inherit
|
||||
&.placeholder
|
||||
width calc(100% - 16px)
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
<style lang="stylus">
|
||||
.placeholder-widget
|
||||
width 100%
|
||||
display inline-block
|
||||
opacity 0.5
|
||||
height calc(2*3rem + 50px)
|
||||
.button
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
<template>
|
||||
<l-circle ref="marker" v-if="center && radius" :key="markerKey" :lat-lng="center" :radius="radius" v-bind="markerConfig" @update:latLng="$emit('update', $event)" @click="performAction">
|
||||
<l-tooltip v-if="config.label">
|
||||
{{config.label}}
|
||||
</l-tooltip>
|
||||
</l-circle>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../widget-mixin'
|
||||
import { LCircle, LTooltip } from 'vue2-leaflet'
|
||||
import { actionGroup, actionProps, actionsMixin } from '../widget-actions'
|
||||
|
||||
export default {
|
||||
mixins: [mixin, actionsMixin],
|
||||
components: {
|
||||
LCircle,
|
||||
LTooltip
|
||||
},
|
||||
widget: {
|
||||
name: 'oh-map-circle-marker',
|
||||
label: 'Circle Marker',
|
||||
icon: 'map_pin_ellipse',
|
||||
description: 'A circle on a map, to represent a radius',
|
||||
props: {
|
||||
parameterGroups: [
|
||||
actionGroup(null, 'Action to perform when the circle is clicked')
|
||||
],
|
||||
parameters: [
|
||||
...actionProps(),
|
||||
{
|
||||
name: 'label',
|
||||
label: 'Label',
|
||||
type: 'TEXT',
|
||||
description: 'The label on the marker'
|
||||
},
|
||||
{
|
||||
name: 'item',
|
||||
label: 'Item',
|
||||
type: 'TEXT',
|
||||
context: 'item',
|
||||
description: 'The Location item this circle will be centered on'
|
||||
},
|
||||
{
|
||||
name: 'location',
|
||||
label: 'Fixed location',
|
||||
type: 'TEXT',
|
||||
context: 'location',
|
||||
description: 'The fixed center of the circle if no item is configured or its coordinates are invalid'
|
||||
},
|
||||
{
|
||||
name: 'radiusItem',
|
||||
label: 'Radius Item',
|
||||
type: 'TEXT',
|
||||
context: 'item',
|
||||
description: 'The item whose state holds the radius of the circle, in meters'
|
||||
},
|
||||
{
|
||||
name: 'radius',
|
||||
label: 'Fixed radius',
|
||||
type: 'DECIMAL',
|
||||
description: 'The fixed radius of the circle in meters if no item is configured or its state is invalid'
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
label: 'Circle color',
|
||||
type: 'TEXT',
|
||||
description: 'The color of the circle (e.g. "blue", "red", "yellow"...)'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
markerKey: this.$f7.utils.id()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
center () {
|
||||
if (this.config.item) {
|
||||
const itemState = this.context.store[this.config.item]
|
||||
if (itemState && itemState.state.indexOf(',') > 0) {
|
||||
return itemState.state.split(',')
|
||||
}
|
||||
}
|
||||
if (this.config.location) {
|
||||
return this.config.location.split(',')
|
||||
}
|
||||
return null
|
||||
},
|
||||
radius () {
|
||||
if (this.config.radiusItem) {
|
||||
const itemState = this.context.store[this.config.radiusItem]
|
||||
if (itemState && !isNaN(parseFloat(itemState.state))) {
|
||||
return parseFloat(itemState.state)
|
||||
}
|
||||
}
|
||||
if (this.config.radius) {
|
||||
return parseFloat(this.config.radius)
|
||||
}
|
||||
return null
|
||||
},
|
||||
markerConfig () {
|
||||
if (!this.config) return {}
|
||||
let ret = {}
|
||||
Object.assign(ret, this.config)
|
||||
delete ret.latLng
|
||||
delete ret.radius
|
||||
return ret
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$emit('update', this.center, this.radius)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<l-marker ref="marker" v-if="coords" :key="markerKey" :lat-lng="coords" @update:latLng="$emit('update', $event)" @click="performAction">
|
||||
<l-tooltip v-if="config.label">
|
||||
{{config.label}}
|
||||
</l-tooltip>
|
||||
<l-icon v-if="icon"
|
||||
:icon-size="[40,40]"
|
||||
:icon-url="icon"
|
||||
/>
|
||||
</l-marker>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mixin from '../widget-mixin'
|
||||
import { LMarker, LTooltip, LIcon } from 'vue2-leaflet'
|
||||
import { actionGroup, actionProps, actionsMixin } from '../widget-actions'
|
||||
|
||||
export default {
|
||||
mixins: [mixin, actionsMixin],
|
||||
components: {
|
||||
LMarker,
|
||||
LTooltip,
|
||||
LIcon
|
||||
},
|
||||
widget: {
|
||||
name: 'oh-map-marker',
|
||||
label: 'Map Marker',
|
||||
icon: 'map_pin',
|
||||
description: 'A marker on a map',
|
||||
props: {
|
||||
parameterGroups: [
|
||||
actionGroup(null, 'Action to perform when the marker is clicked')
|
||||
],
|
||||
parameters: [
|
||||
...actionProps(),
|
||||
{
|
||||
name: 'label',
|
||||
label: 'Label',
|
||||
type: 'TEXT',
|
||||
description: 'The label on the marker'
|
||||
},
|
||||
{
|
||||
name: 'item',
|
||||
label: 'Item',
|
||||
type: 'TEXT',
|
||||
context: 'item',
|
||||
description: 'The Location item this marker will show'
|
||||
},
|
||||
{
|
||||
name: 'location',
|
||||
label: 'Fixed location',
|
||||
type: 'TEXT',
|
||||
context: 'location',
|
||||
description: 'The fixed location to show if no item is configured or its coordinates are invalid'
|
||||
},
|
||||
{
|
||||
name: 'icon',
|
||||
label: 'Icon',
|
||||
type: 'TEXT',
|
||||
description: 'Use <code>oh:iconName</code> (<a class="external text-color-blue" target="_blank" href="https://www.openhab.org/docs/configuration/iconsets/classic/">openHAB icon</a>)'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
markerKey: this.$f7.utils.id()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
coords () {
|
||||
if (this.config.item) {
|
||||
const itemState = this.context.store[this.config.item]
|
||||
if (itemState && itemState.state.indexOf(',') > 0) {
|
||||
return itemState.state.split(',')
|
||||
}
|
||||
}
|
||||
if (this.config.location) {
|
||||
return this.config.location.split(',')
|
||||
}
|
||||
return null
|
||||
},
|
||||
hasIcon () {
|
||||
return this.config.icon
|
||||
}
|
||||
},
|
||||
asyncComputed: {
|
||||
icon () {
|
||||
if (this.config.icon && this.config.icon.indexOf('oh:') === 0) {
|
||||
return this.$oh.media.getIcon(this.config.icon.substring(3)).then((icon) => {
|
||||
// debugger
|
||||
this.markerKey = this.$f7.utils.id()
|
||||
this.$emit('update')
|
||||
return icon
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$emit('update', this.coords)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<l-map
|
||||
ref="map"
|
||||
v-if="showMap"
|
||||
:zoom="zoom"
|
||||
:center="center"
|
||||
:options="mapOptions"
|
||||
class="oh-map-page-lmap"
|
||||
:class="{ 'with-tabbar': context.tab }"
|
||||
@update:center="centerUpdate"
|
||||
@update:zoom="zoomUpdate">
|
||||
<l-tile-layer
|
||||
:url="url"
|
||||
:attribution="attribution"
|
||||
/>
|
||||
<l-feature-group ref="featureGroup" v-if="context.component.slots">
|
||||
<component v-for="(marker, idx) in context.component.slots.default" :key="idx"
|
||||
:is="markerComponent(marker)" :context="childContext(marker)" @update="onMarkerUpdate" />
|
||||
</l-feature-group>
|
||||
</l-map>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.oh-map-page-lmap
|
||||
position absolute
|
||||
top calc(var(--f7-navbar-height))
|
||||
height calc(100% - var(--f7-navbar-height)) !important
|
||||
&.with-tabbar
|
||||
height calc(100% - var(--f7-navbar-height) - var(--f7-tabbar-labels-height)) !important
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import mixin from '../widget-mixin'
|
||||
import { latLng, Icon } from 'leaflet'
|
||||
import { LMap, LTileLayer, LFeatureGroup } from 'vue2-leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
|
||||
import OhMapMarker from './oh-map-marker.vue'
|
||||
import OhMapCircleMarker from './oh-map-circle-marker.vue'
|
||||
|
||||
delete Icon.Default.prototype._getIconUrl
|
||||
Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
|
||||
iconUrl: require('leaflet/dist/images/marker-icon.png'),
|
||||
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
|
||||
})
|
||||
|
||||
export default {
|
||||
mixins: [mixin],
|
||||
components: {
|
||||
LMap,
|
||||
LTileLayer,
|
||||
LFeatureGroup,
|
||||
OhMapMarker,
|
||||
OhMapCircleMarker
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
zoom: 13,
|
||||
currentZoom: 13,
|
||||
currentCenter: null,
|
||||
center: latLng(52.5200066, 13.4049540),
|
||||
// url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
url: `https://a.basemaps.cartocdn.com/${this.$f7.data.themeOptions.dark}_all/{z}/{x}/{y}.png`,
|
||||
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a>, © <a href="https://carto.com/attribution/">CARTO</a>',
|
||||
showMap: true,
|
||||
mapOptions: {
|
||||
zoomSnap: 0.5
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
zoomUpdate (zoom) {
|
||||
this.currentZoom = zoom
|
||||
},
|
||||
centerUpdate (center) {
|
||||
this.currentCenter = center
|
||||
},
|
||||
markerComponent (marker) {
|
||||
switch (marker.component) {
|
||||
case 'oh-map-marker':
|
||||
return OhMapMarker
|
||||
case 'oh-map-circle-marker':
|
||||
return OhMapCircleMarker
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
onMarkerUpdate () {
|
||||
this.$refs.map.mapObject.fitBounds(this.$refs.featureGroup.mapObject.getBounds().pad(0.5))
|
||||
this.$refs.map.mapObject.invalidateSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,9 +1,6 @@
|
|||
<template>
|
||||
<f7-popover :target="el">
|
||||
<oh-layout-page v-if="page && page.component === 'oh-layout-page'" :context="context"
|
||||
class="layout-page"
|
||||
:class="{notready: !ready}" />
|
||||
<generic-widget-component v-else-if="widget" :context="context" :class="{notready: !ready}" />
|
||||
<component :is="componentType" :context="context" :class="{notready: !ready}" />
|
||||
</f7-popover>
|
||||
</template>
|
||||
|
||||
|
@ -41,6 +38,21 @@ export default {
|
|||
},
|
||||
ready () {
|
||||
return this.page || this.widget
|
||||
},
|
||||
componentType () {
|
||||
if (this.page) {
|
||||
switch (this.page.component) {
|
||||
case 'oh-layout-page':
|
||||
return OhLayoutPage
|
||||
case 'oh-map-page':
|
||||
return () => import('@/components/widgets/map/oh-map-page.vue')
|
||||
default:
|
||||
return null
|
||||
}
|
||||
} else if (this.widget) {
|
||||
return 'generic-widget-component'
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,16 @@
|
|||
<f7-navbar :title="(page) ? page.config.label : ''" back-link="Back">
|
||||
</f7-navbar>
|
||||
|
||||
<oh-layout-page v-if="page && page.component === 'oh-layout-page'" :context="context"
|
||||
class="layout-page"
|
||||
:class="{notready: !ready}" />
|
||||
<generic-widget-component v-else-if="widget" :context="context" :class="{notready: !ready}" />
|
||||
<f7-toolbar tabbar labels bottom v-if="page && page.component === 'oh-tabs-page'">
|
||||
<f7-link v-for="(tab, idx) in page.slots.default" :key="idx" tab-link @click="currentTab = idx" :tab-link-active="currentTab === idx" :icon-ios="tab.config.icon" :icon-md="tab.config.icon" :icon-aurora="tab.config.icon" :text="tab.config.title"></f7-link>
|
||||
</f7-toolbar>
|
||||
|
||||
<f7-tabs v-if="page && page.component === 'oh-tabs-page'" :class="{notready: !ready}">
|
||||
<f7-tab v-for="(tab, idx) in page.slots.default" :key="idx" :tab-active="currentTab === idx">
|
||||
<component v-if="currentTab === idx" :is="tabComponent(tab)" :context="tabContext(tab)" />
|
||||
</f7-tab>
|
||||
</f7-tabs>
|
||||
<component v-else :is="componentType" :context="context" :class="{notready: !ready}" />
|
||||
|
||||
</f7-page>
|
||||
</f7-popup>
|
||||
|
@ -23,11 +29,13 @@ import OhLayoutPage from '@/components/widgets/layout/oh-layout-page.vue'
|
|||
|
||||
export default {
|
||||
components: {
|
||||
OhLayoutPage
|
||||
OhLayoutPage,
|
||||
'oh-map-page': () => import('@/components/widgets/map/oh-map-page.vue')
|
||||
},
|
||||
props: ['uid', 'modalParams'],
|
||||
data () {
|
||||
return {
|
||||
currentTab: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -46,6 +54,47 @@ export default {
|
|||
},
|
||||
ready () {
|
||||
return this.page || this.widget
|
||||
},
|
||||
componentType () {
|
||||
if (this.page) {
|
||||
switch (this.page.component) {
|
||||
case 'oh-layout-page':
|
||||
return OhLayoutPage
|
||||
case 'oh-map-page':
|
||||
return 'oh-map-page'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
} else if (this.widget) {
|
||||
return 'generic-widget-component'
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tabContext (tab) {
|
||||
const page = this.$store.getters.page(tab.config.page.replace('page:', ''))
|
||||
return {
|
||||
component: page,
|
||||
tab: tab,
|
||||
props: tab.config.pageConfig,
|
||||
store: this.$store.getters.trackedItems
|
||||
}
|
||||
},
|
||||
pageComponent (page) {
|
||||
if (!page) return null
|
||||
switch (page.component) {
|
||||
case 'oh-layout-page':
|
||||
return OhLayoutPage
|
||||
case 'oh-map-page':
|
||||
return 'oh-map-page'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
tabComponent (tab) {
|
||||
const page = this.$store.getters.page(tab.config.page.replace('page:', ''))
|
||||
return this.pageComponent(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,7 @@
|
|||
<div class="right"><f7-link sheet-close>Close</f7-link></div>
|
||||
</f7-toolbar>
|
||||
|
||||
<f7-page v-if="page && page.component === 'oh-layout-page'">
|
||||
<oh-layout-page :context="context"
|
||||
class="layout-page"
|
||||
:class="{notready: !ready}" />
|
||||
</f7-page>
|
||||
<generic-widget-component v-else-if="widget" :context="context" :class="{notready: !ready}" />
|
||||
<component :is="componentType" :context="context" :class="{notready: !ready}" />
|
||||
</f7-sheet>
|
||||
</template>
|
||||
|
||||
|
@ -47,6 +42,21 @@ export default {
|
|||
},
|
||||
ready () {
|
||||
return this.page || this.widget
|
||||
},
|
||||
componentType () {
|
||||
if (this.page) {
|
||||
switch (this.page.component) {
|
||||
case 'oh-layout-page':
|
||||
return OhLayoutPage
|
||||
case 'oh-map-page':
|
||||
return () => import('@/components/widgets/map/oh-map-page.vue')
|
||||
default:
|
||||
return null
|
||||
}
|
||||
} else if (this.widget) {
|
||||
return 'generic-widget-component'
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,11 @@ export default {
|
|||
evalConfig[key] = expr.eval(this.exprAst[key], {
|
||||
items: this.context.store,
|
||||
props: this.context.props,
|
||||
Math: Math
|
||||
Math: Math,
|
||||
theme: this.$theme,
|
||||
themeOptions: this.$f7.data.themeOptions,
|
||||
device: this.$device,
|
||||
JSON: JSON
|
||||
})
|
||||
} catch (e) {
|
||||
evalConfig[key] = e
|
||||
|
|
|
@ -2,8 +2,8 @@ import HomePage from '../pages/home.vue'
|
|||
import AboutPage from '../pages/about.vue'
|
||||
import NotFoundPage from '../pages/not-found.vue'
|
||||
|
||||
import SitemapPage from '../pages/page/sitemap.vue'
|
||||
import LayoutPage from '../pages/page/layout-page.vue'
|
||||
import SitemapViewPage from '../pages/page/sitemap-view.vue'
|
||||
import PageViewPage from '../pages/page/page-view.vue'
|
||||
|
||||
import SetupWizard from '../pages/wizards/setup-wizard.vue'
|
||||
import SetupWizardPage from '../pages/wizards/setup-wizard-page.vue'
|
||||
|
@ -33,8 +33,6 @@ import RuleEditPage from '../pages/settings/rules/rule-edit.vue'
|
|||
import RuleConfigureModulePage from '../pages/settings/rules/rule-configure-module.vue'
|
||||
|
||||
import PagesListPage from '../pages/settings/pages/pages-list.vue'
|
||||
import SitemapEditPage from '../pages/settings/pages/sitemap/sitemap-edit.vue'
|
||||
import LayoutEditPage from '../pages/settings/pages/layout/layout-edit.vue'
|
||||
|
||||
// import SchedulePage from '../pages/settings/schedule/schedule.vue'
|
||||
|
||||
|
@ -62,11 +60,11 @@ export default [
|
|||
},
|
||||
{
|
||||
path: '/page/:uid',
|
||||
component: LayoutPage
|
||||
component: PageViewPage
|
||||
},
|
||||
{
|
||||
path: '/sitemap/:sitemapId/:pageId',
|
||||
component: SitemapPage
|
||||
component: SitemapViewPage
|
||||
},
|
||||
{
|
||||
path: '/about/',
|
||||
|
@ -121,30 +119,23 @@ export default [
|
|||
component: PagesListPage,
|
||||
routes: [
|
||||
{
|
||||
path: 'sitemap/add',
|
||||
component: SitemapEditPage,
|
||||
options: {
|
||||
path: ':type/:uid',
|
||||
async (routeTo, routeFrom, resolve, reject) {
|
||||
// dynamic import component; returns promise
|
||||
const editorComponent = () => import(`../pages/settings/pages/${routeTo.params.type}/${routeTo.params.type}-edit.vue`)
|
||||
// resolve promise
|
||||
editorComponent().then((vc) => {
|
||||
// resolve with component
|
||||
resolve({
|
||||
component: vc.default
|
||||
},
|
||||
(routeTo.params.uid === 'add') ? {
|
||||
props: {
|
||||
createMode: true
|
||||
}
|
||||
} : {})
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'sitemap/:uid',
|
||||
component: SitemapEditPage
|
||||
},
|
||||
{
|
||||
path: 'layout/add',
|
||||
component: LayoutEditPage,
|
||||
options: {
|
||||
props: {
|
||||
createMode: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'layout/:uid',
|
||||
component: LayoutEditPage
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
<template>
|
||||
<f7-page @page:afterin="onPageAfterIn" @page:beforeout="onPageBeforeOut" hide-bars-on-scroll>
|
||||
<f7-navbar :back-link="(deep) ? 'Back' : undefined">
|
||||
<f7-nav-left v-if="!deep">
|
||||
<f7-link icon-ios="f7:menu" icon-aurora="f7:menu" icon-md="material:menu" panel-open="left"></f7-link>
|
||||
</f7-nav-left>
|
||||
<f7-nav-title>{{(ready) ? page.config.label : ''}}</f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link icon-md="material:edit" :href="'/settings/pages/' + pageType + '/' + uid">{{ $theme.md ? '' : 'Edit' }}</f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
|
||||
<oh-layout-page v-if="page && pageType === 'layout'" :context="context"
|
||||
class="layout-page"
|
||||
:class="{notready: !ready}"
|
||||
@command="onCommand" />
|
||||
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.notready
|
||||
visibility hidden
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import OhLayoutPage from '@/components/widgets/layout/oh-layout-page.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OhLayoutPage
|
||||
},
|
||||
props: ['uid', 'deep'],
|
||||
data () {
|
||||
return {
|
||||
// ready: false,
|
||||
loading: false
|
||||
// page: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
context () {
|
||||
return {
|
||||
component: this.page,
|
||||
store: this.$store.getters.trackedItems
|
||||
}
|
||||
},
|
||||
page () {
|
||||
return this.$store.getters.page(this.uid)
|
||||
},
|
||||
pageType () {
|
||||
switch (this.page.component) {
|
||||
case 'oh-layout-page':
|
||||
return 'layout'
|
||||
default:
|
||||
console.warn('Unknown page type!')
|
||||
return 'unknown'
|
||||
}
|
||||
},
|
||||
ready () {
|
||||
return this.page
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onPageAfterIn () {
|
||||
this.$store.dispatch('startTrackingStates')
|
||||
this.load()
|
||||
},
|
||||
onPageBeforeOut () {
|
||||
this.$store.dispatch('stopTrackingStates')
|
||||
},
|
||||
onCommand (itemName, command) {
|
||||
this.$store.dispatch('sendCommand', { itemName, command })
|
||||
},
|
||||
load () {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<f7-page @page:afterin="onPageAfterIn" @page:beforeout="onPageBeforeOut" hide-bars-on-scroll>
|
||||
<f7-navbar :back-link="(deep) ? 'Back' : undefined">
|
||||
<f7-nav-left v-if="!deep">
|
||||
<f7-link icon-ios="f7:menu" icon-aurora="f7:menu" icon-md="material:menu" panel-open="left"></f7-link>
|
||||
</f7-nav-left>
|
||||
<f7-nav-title>{{(ready) ? page.config.label : ''}}</f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link icon-md="material:edit" :href="'/settings/pages/' + pageType + '/' + uid">{{ $theme.md ? '' : 'Edit' }}</f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
|
||||
<f7-toolbar tabbar labels bottom v-if="page && pageType === 'tabs'">
|
||||
<f7-link v-for="(tab, idx) in page.slots.default" :key="idx" tab-link @click="currentTab = idx" :tab-link-active="currentTab === idx" :icon-ios="tab.config.icon" :icon-md="tab.config.icon" :icon-aurora="tab.config.icon" :text="tab.config.title"></f7-link>
|
||||
</f7-toolbar>
|
||||
|
||||
<f7-tabs v-if="page && pageType === 'tabs'" :class="{notready: !ready}">
|
||||
<f7-tab v-for="(tab, idx) in page.slots.default" :key="idx" :tab-active="currentTab === idx">
|
||||
<component v-if="currentTab === idx" :is="tabComponent(tab)" :context="tabContext(tab)" @command="onCommand" />
|
||||
</f7-tab>
|
||||
</f7-tabs>
|
||||
|
||||
<component :is="pageComponent(page)" v-if="page" :context="context" :class="{notready: !ready}" @command="onCommand" />
|
||||
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.notready
|
||||
visibility hidden
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import OhLayoutPage from '@/components/widgets/layout/oh-layout-page.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OhLayoutPage,
|
||||
'oh-map-page': () => import('@/components/widgets/map/oh-map-page.vue')
|
||||
},
|
||||
props: ['uid', 'deep'],
|
||||
data () {
|
||||
return {
|
||||
currentTab: 0,
|
||||
// ready: false,
|
||||
loading: false
|
||||
// page: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
context () {
|
||||
return {
|
||||
component: this.page,
|
||||
store: this.$store.getters.trackedItems
|
||||
}
|
||||
},
|
||||
page () {
|
||||
return this.$store.getters.page(this.uid)
|
||||
},
|
||||
pageType () {
|
||||
if (!this.page) return null
|
||||
switch (this.page.component) {
|
||||
case 'oh-layout-page':
|
||||
return 'layout'
|
||||
case 'oh-map-page':
|
||||
return 'map'
|
||||
case 'oh-tabs-page':
|
||||
return 'tabs'
|
||||
default:
|
||||
console.warn('Unknown page type!')
|
||||
return 'unknown'
|
||||
}
|
||||
},
|
||||
ready () {
|
||||
return this.page
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onPageAfterIn () {
|
||||
this.$store.dispatch('startTrackingStates')
|
||||
this.load()
|
||||
},
|
||||
onPageBeforeOut () {
|
||||
this.$store.dispatch('stopTrackingStates')
|
||||
},
|
||||
onCommand (itemName, command) {
|
||||
this.$store.dispatch('sendCommand', { itemName, command })
|
||||
},
|
||||
load () {
|
||||
},
|
||||
tabContext (tab) {
|
||||
const page = this.$store.getters.page(tab.config.page.replace('page:', ''))
|
||||
return {
|
||||
component: page,
|
||||
tab: tab,
|
||||
props: tab.config.pageConfig,
|
||||
store: this.$store.getters.trackedItems
|
||||
}
|
||||
},
|
||||
pageComponent (page) {
|
||||
if (!page) return null
|
||||
switch (page.component) {
|
||||
case 'oh-layout-page':
|
||||
return OhLayoutPage
|
||||
case 'oh-map-page':
|
||||
return 'oh-map-page'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
tabComponent (tab) {
|
||||
const page = this.$store.getters.page(tab.config.page.replace('page:', ''))
|
||||
return this.pageComponent(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<f7-page @page:afterin="onPageAfterIn" @page:beforeout="onPageBeforeOut">
|
||||
<f7-page @page:afterin="onPageAfterIn" @page:beforeout="onPageBeforeOut" class="layout-editor">
|
||||
<f7-navbar :title="(!ready) ? '' : (createMode) ? 'Create layout page' : page.config.label" back-link="Back" no-hairline>
|
||||
<f7-nav-right>
|
||||
<f7-link @click="save()" v-if="$theme.md" icon-md="material:save" icon-only></f7-link>
|
||||
|
@ -117,12 +117,15 @@
|
|||
position absolute
|
||||
top 80%
|
||||
white-space pre-wrap
|
||||
.menu-dropdown-content
|
||||
z-index 2000
|
||||
.layout-editor-design-tab
|
||||
.layout-page
|
||||
.oh-columns-grid
|
||||
.oh-masonry
|
||||
z-index inherit
|
||||
padding-bottom 5rem
|
||||
.layout-editor
|
||||
.page-content
|
||||
padding-bottom 5rem
|
||||
z-index inherit
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -0,0 +1,477 @@
|
|||
<template>
|
||||
<f7-page @page:afterin="onPageAfterIn" @page:beforeout="onPageBeforeOut" class="map-editor">
|
||||
<f7-navbar :title="(!ready) ? '' : (createMode) ? 'Create map page' : page.config.label" back-link="Back" no-hairline>
|
||||
<f7-nav-right>
|
||||
<f7-link @click="save()" v-if="$theme.md" icon-md="material:save" icon-only></f7-link>
|
||||
<f7-link @click="save()" v-if="!$theme.md">Save<span v-if="$device.desktop"> (Ctrl-S)</span></f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
<f7-toolbar tabbar position="top">
|
||||
<f7-link @click="currentTab = 'design'; fromYaml()" :tab-link-active="currentTab === 'design'" class="tab-link">Design</f7-link>
|
||||
<!-- <f7-link @click="currentTab = 'preview'" :tab-link-active="currentTab === 'preview'" class="tab-link">Preview</f7-link> -->
|
||||
<f7-link @click="currentTab = 'code'; toYaml()" :tab-link-active="currentTab === 'code'" class="tab-link">Code</f7-link>
|
||||
</f7-toolbar>
|
||||
<f7-toolbar bottom class="toolbar-details" v-show="currentTab === 'design'">
|
||||
<div style="margin-left: auto">
|
||||
<f7-toggle :checked="previewMode" @toggle:change="(value) => previewMode = value"></f7-toggle> Run mode<span v-if="$device.desktop"> (Ctrl-R)</span>
|
||||
</div>
|
||||
</f7-toolbar>
|
||||
<f7-tabs class="map-editor-tabs">
|
||||
<f7-tab id="design" class="map-editor-design-tab" @tab:show="() => this.currentTab = 'design'" :tab-active="currentTab === 'design'">
|
||||
<f7-block v-if="!ready" class="text-align-center">
|
||||
<f7-preloader></f7-preloader>
|
||||
<div>Loading...</div>
|
||||
</f7-block>
|
||||
<f7-block class="block-narrow" v-if="ready && !previewMode">
|
||||
<f7-col>
|
||||
<f7-list inline-labels>
|
||||
<f7-list-input label="ID" type="text" placeholder="ID" :value="page.uid" @input="page.uid = $event.target.value"
|
||||
required validate pattern="[A-Za-z0-9_]+" error-message="Required. Alphanumeric & underscores only" :disabled="!createMode">
|
||||
</f7-list-input>
|
||||
<f7-list-input label="Label" type="text" placeholder="Label" :value="page.config.label" @input="page.config.label = $event.target.value" clear-button>
|
||||
</f7-list-input>
|
||||
<f7-list-item title="Show on sidebar">
|
||||
<f7-toggle slot="after" :checked="page.config.sidebar" @toggle:change="page.config.sidebar = $event"></f7-toggle>
|
||||
</f7-list-item>
|
||||
<f7-list-input label="Sidebar order" type="number" placeholder="Assign order index to rearrange pages on sidebar" :value="page.config.order" @input="page.config.order = $event.target.value" clear-button>
|
||||
</f7-list-input>
|
||||
</f7-list>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
|
||||
<f7-block class="block-narrow" v-if="ready && !previewMode">
|
||||
<f7-col>
|
||||
<f7-block-title>Markers</f7-block-title>
|
||||
<f7-menu v-if="clipboardType === 'oh-map-marker'">
|
||||
<f7-menu-item style="margin-left: auto" icon-f7="map" dropdown>
|
||||
<f7-menu-dropdown right>
|
||||
<f7-menu-dropdown-item @click="pasteWidget(page, null)" href="#" text="Paste"></f7-menu-dropdown-item>
|
||||
</f7-menu-dropdown>
|
||||
</f7-menu-item>
|
||||
</f7-menu>
|
||||
|
||||
<f7-list media-list>
|
||||
<f7-list-item media-item v-for="(marker, idx) in page.slots.default" :key="idx"
|
||||
:title="marker.config.label" :subtitle="marker.config.item || marker.config.location">
|
||||
<oh-icon v-if="marker.config.icon && marker.config.icon.indexOf('oh:') === 0" slot="media" :icon="marker.config.icon.substring(3)" height="32" width="32" />
|
||||
<f7-icon v-else slot="media" :f7="markerDefaultIcon(marker)" :size="32" />
|
||||
<f7-menu slot="content-start">
|
||||
<f7-menu-item icon-f7="list_bullet" dropdown>
|
||||
<f7-menu-dropdown>
|
||||
<f7-menu-dropdown-item @click="configureWidget(marker, { component: page })" href="#" text="Configure marker"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="editWidgetCode(marker, { component: page })" href="#" text="Edit YAML"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item divider></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="cutWidget(marker, { component: page })" href="#" text="Cut"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="copyWidget(marker, { component: page })" href="#" text="Copy"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item divider></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="moveWidgetUp(marker, { component: page })" href="#" text="Move Up"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="moveWidgetDown(marker, { component: page })" href="#" text="Move Down"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item divider></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="removeWidget(marker, { component: page })" href="#" text="Remove marker"></f7-menu-dropdown-item>
|
||||
</f7-menu-dropdown>
|
||||
</f7-menu-item>
|
||||
</f7-menu>
|
||||
</f7-list-item>
|
||||
<f7-list-button color="blue" title="Add marker" @click="addWidget(page, 'oh-map-marker')" />
|
||||
<f7-list-button color="blue" title="Add circle marker" @click="addWidget(page, 'oh-map-circle-marker')" />
|
||||
<!-- <f7-list-button color="blue" title="Add marker" @click="addWidget(page, 'oh-map-radius')" /> -->
|
||||
</f7-list>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
|
||||
<oh-map-page class="map-page" v-else-if="ready && previewMode" :context="context" :key="pageKey" />
|
||||
|
||||
</f7-tab>
|
||||
|
||||
<!-- <f7-tab id="preview" class="map-editor-preview-tab" @tab:show="() => this.currentTab = 'preview'" :tab-active="currentTab === 'preview'">
|
||||
</f7-tab> -->
|
||||
|
||||
<f7-tab id="code" @tab:show="() => { this.currentTab = 'code' }" :tab-active="currentTab === 'code'">
|
||||
<editor v-if="currentTab === 'code'" class="page-code-editor" mode="text/x-yaml" :value="pageYaml" @input="(value) => pageYaml = value" />
|
||||
<pre class="yaml-message padding-horizontal" :class="[yamlError === 'OK' ? 'text-color-green' : 'text-color-red']">{{yamlError}}</pre>
|
||||
</f7-tab>
|
||||
|
||||
</f7-tabs>
|
||||
|
||||
<f7-popup ref="widgetConfig" class="widgetconfig-popup" close-on-escape :opened="widgetConfigOpened" @popup:closed="widgetConfigClosed">
|
||||
<f7-page v-if="currentComponent && currentWidget">
|
||||
<f7-navbar>
|
||||
<f7-nav-left>
|
||||
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" popup-close></f7-link>
|
||||
</f7-nav-left>
|
||||
<f7-nav-title>Edit {{currentWidget.label || currentWidget.uid}}</f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link @click="updateWidgetConfig">Done</f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
<f7-block v-if="currentWidget.props">
|
||||
<f7-col>
|
||||
<config-sheet
|
||||
:parameterGroups="currentWidget.props.parameterGroups || []"
|
||||
:parameters="currentWidget.props.parameters || []"
|
||||
:configuration="currentComponentConfig"
|
||||
@updated="dirty = true"
|
||||
/>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
</f7-page>
|
||||
</f7-popup>
|
||||
|
||||
<f7-popup ref="widgetCode" class="widgetcode-popup" close-on-escape :opened="widgetCodeOpened" @popup:closed="widgetCodeClosed">
|
||||
<f7-page v-if="currentComponent && widgetCodeOpened">
|
||||
<f7-navbar>
|
||||
<f7-nav-left>
|
||||
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" popup-close></f7-link>
|
||||
</f7-nav-left>
|
||||
<f7-nav-title>Edit Widget Code</f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link @click="updateWidgetCode">Done</f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
<editor class="page-code-editor" mode="text/x-yaml" :value="widgetYaml" @input="(value) => widgetYaml = value" />
|
||||
<pre class="yaml-message padding-horizontal" :class="[widgetYamlError === 'OK' ? 'text-color-green' : 'text-color-red']">{{widgetYamlError}}</pre>
|
||||
</f7-page>
|
||||
</f7-popup>
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.page-code-editor.vue-codemirror
|
||||
display block
|
||||
top calc(var(--f7-navbar-height) + var(--f7-tabbar-height))
|
||||
height calc(80% - 2*var(--f7-navbar-height))
|
||||
width 100%
|
||||
.yaml-message
|
||||
display block
|
||||
position absolute
|
||||
top 80%
|
||||
white-space pre-wrap
|
||||
.map-editor
|
||||
.oh-map-page-lmap
|
||||
top calc(var(--f7-navbar-height) + var(--f7-toolbar-height)) !important
|
||||
height calc(100% - var(--f7-navbar-height) - 2 * var(--f7-toolbar-height)) !important
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import YAML from 'yaml'
|
||||
|
||||
// import OhMapPage from '@/components/widgets/map/oh-map-page.vue'
|
||||
import OhMapMarker from '@/components/widgets/map/oh-map-marker.vue'
|
||||
import OhMapCircleMarker from '@/components/widgets/map/oh-map-circle-marker.vue'
|
||||
|
||||
// const ConfigurableWidgets = {
|
||||
// 'oh-map-marker': () => import('@/components/widgets/map/oh-map-marker.vue'),
|
||||
// 'oh-map-circle-marker': () => import('@/components/widgets/map/oh-map-circle-marker.vue')
|
||||
// }
|
||||
const ConfigurableWidgets = {
|
||||
OhMapMarker,
|
||||
OhMapCircleMarker
|
||||
}
|
||||
|
||||
import ConfigSheet from '@/components/config/config-sheet.vue'
|
||||
|
||||
function uuidv4 () {
|
||||
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'editor': () => import('@/components/config/controls/script-editor.vue'),
|
||||
'oh-map-page': () => import('@/components/widgets/map/oh-map-page.vue'),
|
||||
ConfigSheet
|
||||
},
|
||||
props: ['createMode', 'uid'],
|
||||
data () {
|
||||
return {
|
||||
pageReady: false,
|
||||
loading: false,
|
||||
page: {
|
||||
uid: 'page_' + uuidv4().split('-')[0],
|
||||
component: 'oh-map-page',
|
||||
config: {},
|
||||
slots: { default: [] }
|
||||
},
|
||||
pageKey: uuidv4(),
|
||||
pageYaml: null,
|
||||
previewMode: false,
|
||||
currentTab: 'design',
|
||||
clipboard: null,
|
||||
clipboardType: null,
|
||||
currentComponent: null,
|
||||
currentComponentConfig: null,
|
||||
currentWidget: null,
|
||||
widgetConfigOpened: false,
|
||||
widgetCodeOpened: false,
|
||||
widgetYaml: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ready () {
|
||||
return this.pageReady && this.$store.state.components.widgets != null
|
||||
},
|
||||
context () {
|
||||
return {
|
||||
component: this.page,
|
||||
store: this.$store.getters.trackedItems,
|
||||
// states: this.stateTracking.store,
|
||||
editmode: (!this.previewMode) ? {
|
||||
addWidget: this.addWidget,
|
||||
configureWidget: this.configureWidget,
|
||||
editWidgetCode: this.editWidgetCode,
|
||||
cutWidget: this.cutWidget,
|
||||
copyWidget: this.copyWidget,
|
||||
pasteWidget: this.pasteWidget,
|
||||
moveWidgetUp: this.moveWidgetUp,
|
||||
moveWidgetDown: this.moveWidgetDown,
|
||||
removeWidget: this.removeWidget
|
||||
} : null,
|
||||
clipboardtype: this.clipboardType
|
||||
}
|
||||
},
|
||||
yamlError () {
|
||||
if (this.currentTab !== 'code') return null
|
||||
try {
|
||||
YAML.parse(this.pageYaml, { prettyErrors: true })
|
||||
return 'OK'
|
||||
} catch (e) {
|
||||
return e
|
||||
}
|
||||
},
|
||||
widgetYamlError () {
|
||||
if (!this.widgetCodeOpened) return null
|
||||
try {
|
||||
YAML.parse(this.widgetYaml, { prettyErrors: true })
|
||||
return 'OK'
|
||||
} catch (e) {
|
||||
return e
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onPageAfterIn () {
|
||||
if (window) {
|
||||
window.addEventListener('keydown', this.keyDown)
|
||||
}
|
||||
this.$store.dispatch('startTrackingStates')
|
||||
this.load()
|
||||
},
|
||||
onPageBeforeOut () {
|
||||
if (window) {
|
||||
window.removeEventListener('keydown', this.keyDown)
|
||||
}
|
||||
this.$store.dispatch('stopTrackingStates')
|
||||
},
|
||||
keyDown (ev) {
|
||||
if (ev.ctrlKey || ev.metakKey) {
|
||||
switch (ev.keyCode) {
|
||||
case 82:
|
||||
this.previewMode = !this.previewMode
|
||||
ev.stopPropagation()
|
||||
ev.preventDefault()
|
||||
break
|
||||
case 83:
|
||||
this.save(!this.createMode)
|
||||
ev.stopPropagation()
|
||||
ev.preventDefault()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
load () {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
if (this.createMode) {
|
||||
this.loading = false
|
||||
this.pageReady = true
|
||||
} else {
|
||||
this.$oh.api.get('/rest/ui/components/ui:page/' + this.uid).then((data) => {
|
||||
this.$set(this, 'page', data)
|
||||
this.pageReady = true
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
save (stay) {
|
||||
if (!this.page.uid) {
|
||||
this.$f7.dialog.alert('Please give an ID to the page')
|
||||
return
|
||||
}
|
||||
if (!this.page.config.label) {
|
||||
this.$f7.dialog.alert('Please give an label to the page')
|
||||
return
|
||||
}
|
||||
if (!this.createMode && this.uid !== this.page.uid) {
|
||||
this.$f7.dialog.alert('You cannot change the ID of an existing page. Duplicate it with the new ID then delete this one.')
|
||||
return
|
||||
}
|
||||
|
||||
const promise = (this.createMode)
|
||||
? this.$oh.api.postPlain('/rest/ui/components/ui:page', JSON.stringify(this.page), 'text/plain', 'application/json')
|
||||
: this.$oh.api.put('/rest/ui/components/ui:page/' + this.page.uid, this.page)
|
||||
promise.then((data) => {
|
||||
if (this.createMode) {
|
||||
this.$f7.toast.create({
|
||||
text: 'Page created',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
this.load()
|
||||
} else {
|
||||
this.$f7.toast.create({
|
||||
text: 'Page updated',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
}
|
||||
this.$f7.emit('sidebarRefresh', null)
|
||||
if (!stay) this.$f7router.back()
|
||||
}).catch((err) => {
|
||||
this.$f7.toast.create({
|
||||
text: 'Error while saving page: ' + err,
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
})
|
||||
},
|
||||
markerDefaultIcon (marker) {
|
||||
const widgetDefinition = Object.values(ConfigurableWidgets).find((c) => c.widget.name === marker.component)
|
||||
if (widgetDefinition) {
|
||||
return widgetDefinition.widget.icon
|
||||
}
|
||||
return null
|
||||
},
|
||||
addWidget (component, widgetType, parentContext, slot) {
|
||||
if (!slot) slot = 'default'
|
||||
if (!component.slots) component.slots = {}
|
||||
if (!component.slots[slot]) component.slots[slot] = []
|
||||
if (widgetType) {
|
||||
component.slots[slot].push({
|
||||
component: widgetType,
|
||||
config: {},
|
||||
slots: { default: [] }
|
||||
})
|
||||
this.forceUpdate()
|
||||
}
|
||||
},
|
||||
widgetConfigClosed () {
|
||||
this.currentComponent = null
|
||||
this.currentWidget = null
|
||||
this.widgetConfigOpened = false
|
||||
},
|
||||
updateWidgetConfig () {
|
||||
this.$set(this.currentComponent, 'config', this.currentComponentConfig)
|
||||
this.forceUpdate()
|
||||
this.widgetConfigClosed()
|
||||
},
|
||||
widgetCodeClosed () {
|
||||
this.currentComponent = null
|
||||
this.currentWidget = null
|
||||
this.widgetCodeOpened = false
|
||||
},
|
||||
updateWidgetCode () {
|
||||
const updatedWidget = YAML.parse(this.widgetYaml)
|
||||
this.$set(this.currentComponent, 'config', updatedWidget.config)
|
||||
this.$set(this.currentComponent, 'slots', updatedWidget.slots)
|
||||
this.forceUpdate()
|
||||
this.widgetCodeClosed()
|
||||
},
|
||||
configureWidget (component, parentContext, forceComponentType) {
|
||||
const componentType = forceComponentType || component.component
|
||||
this.currentComponent = null
|
||||
this.currentWidget = null
|
||||
let widgetDefinition
|
||||
if (componentType.indexOf('widget:') === 0) {
|
||||
this.currentWidget = this.$store.getters.widget(componentType.substring(7))
|
||||
} else {
|
||||
widgetDefinition = Object.values(ConfigurableWidgets).find((w) => w.widget && w.widget.name === componentType)
|
||||
if (!widgetDefinition) {
|
||||
// widgetDefinition = Object.values(LayoutWidgets).find((w) => w.widget.name === component.component)
|
||||
if (!widgetDefinition) {
|
||||
console.warn('Widget not found: ' + componentType)
|
||||
this.$f7.toast.create({
|
||||
text: `This type of component cannot be configured: ${componentType}.`,
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 3000,
|
||||
closeButton: true,
|
||||
closeButtonText: 'Edit YAML',
|
||||
on: {
|
||||
closeButtonClick: () => {
|
||||
this.editWidgetCode(component, parentContext)
|
||||
}
|
||||
}
|
||||
}).open()
|
||||
return
|
||||
}
|
||||
}
|
||||
this.currentWidget = widgetDefinition.widget
|
||||
}
|
||||
this.currentComponent = component
|
||||
this.currentComponentConfig = JSON.parse(JSON.stringify(this.currentComponent.config))
|
||||
this.widgetConfigOpened = true
|
||||
},
|
||||
editWidgetCode (component, parentContext, slot) {
|
||||
if (slot && !component.slots) component.slots = {}
|
||||
if (slot && !component.slots[slot]) component.slots[slot] = []
|
||||
this.currentComponent = component
|
||||
this.widgetYaml = YAML.stringify(component)
|
||||
this.widgetCodeOpened = true
|
||||
},
|
||||
cutWidget (component, parentContext) {
|
||||
this.copyWidget(component, parentContext)
|
||||
this.removeWidget(component, parentContext)
|
||||
},
|
||||
copyWidget (component, parentContext) {
|
||||
let newClipboard = JSON.stringify(component)
|
||||
this.$set(this, 'clipboard', newClipboard)
|
||||
this.clipboardType = component.component
|
||||
},
|
||||
pasteWidget (component, parentContext) {
|
||||
if (!this.clipboard) return
|
||||
component.slots.default.push(JSON.parse(this.clipboard))
|
||||
this.forceUpdate()
|
||||
},
|
||||
moveWidgetUp (component, parentContext) {
|
||||
let siblings = parentContext.component.slots.default
|
||||
let pos = siblings.indexOf(component)
|
||||
if (pos <= 0) return
|
||||
siblings.splice(pos, 1)
|
||||
siblings.splice(pos - 1, 0, component)
|
||||
this.forceUpdate()
|
||||
},
|
||||
moveWidgetDown (component, parentContext) {
|
||||
let siblings = parentContext.component.slots.default
|
||||
let pos = siblings.indexOf(component)
|
||||
if (pos >= siblings.length - 1) return
|
||||
siblings.splice(pos, 1)
|
||||
siblings.splice(pos + 1, 0, component)
|
||||
this.forceUpdate()
|
||||
},
|
||||
removeWidget (component, parentContext) {
|
||||
parentContext.component.slots.default.splice(parentContext.component.slots.default.indexOf(component), 1)
|
||||
this.forceUpdate()
|
||||
},
|
||||
forceUpdate () {
|
||||
this.pageKey = uuidv4()
|
||||
},
|
||||
toYaml () {
|
||||
this.pageYaml = YAML.stringify({
|
||||
markers: this.page.slots.default
|
||||
})
|
||||
},
|
||||
fromYaml () {
|
||||
try {
|
||||
const updatedMarkers = YAML.parse(this.pageYaml)
|
||||
this.$set(this.page.slots, 'default', updatedMarkers.markers)
|
||||
this.forceUpdate()
|
||||
return true
|
||||
} catch (e) {
|
||||
this.$f7.dialog.alert(e).open()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -71,6 +71,7 @@
|
|||
:link="showCheckboxes ? null : getPageType(page).type + '/' + page.uid"
|
||||
:title="page.config.label"
|
||||
:subtitle="getPageType(page).label"
|
||||
:footer="page.uid"
|
||||
>
|
||||
<div slot="subtitle">
|
||||
<f7-chip v-for="tag in page.tags" :key="tag" :text="tag" media-bg-color="blue" style="margin-right: 6px">
|
||||
|
@ -92,8 +93,9 @@
|
|||
<f7-fab-buttons position="top">
|
||||
<f7-fab-button fab-close label="Create sitemap" href="sitemap/add"><f7-icon f7="menu"></f7-icon></f7-fab-button>
|
||||
<f7-fab-button fab-close label="Create layout" href="layout/add"><f7-icon f7="rectangle_grid_2x2"></f7-icon></f7-fab-button>
|
||||
<!-- <f7-fab-button fab-close label="Create map view" href="add"><f7-icon f7="map"></f7-icon></f7-fab-button>
|
||||
<f7-fab-button fab-close label="Create chart" href="add"><f7-icon f7="graph_square"></f7-icon></f7-fab-button>
|
||||
<f7-fab-button fab-close label="Create tabbed page" href="tabs/add"><f7-icon f7="squares_below_rectangle"></f7-icon></f7-fab-button>
|
||||
<f7-fab-button fab-close label="Create map view" href="map/add"><f7-icon f7="map"></f7-icon></f7-fab-button>
|
||||
<!-- <f7-fab-button fab-close label="Create chart" href="add"><f7-icon f7="graph_square"></f7-icon></f7-fab-button>
|
||||
<f7-fab-button fab-close label="Create floor plan" href="add"><f7-icon f7="layers"></f7-icon></f7-fab-button> -->
|
||||
</f7-fab-buttons>
|
||||
</f7-fab>
|
||||
|
@ -112,7 +114,9 @@ export default {
|
|||
showCheckboxes: false,
|
||||
pageTypes: [
|
||||
{ type: 'sitemap', label: 'Sitemap', componentType: 'Sitemap' },
|
||||
{ type: 'layout', label: 'Layout', componentType: 'oh-layout-page' }
|
||||
{ type: 'layout', label: 'Layout', componentType: 'oh-layout-page' },
|
||||
{ type: 'tabs', label: 'Tabbed', componentType: 'oh-tabs-page' },
|
||||
{ type: 'map', label: 'Map', componentType: 'oh-map-page' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -130,6 +134,12 @@ export default {
|
|||
switch (page.component) {
|
||||
case 'Sitemap':
|
||||
return 'menu'
|
||||
case 'oh-layout-page':
|
||||
return 'rectangle_grid_2x2'
|
||||
case 'oh-tabs-page':
|
||||
return 'squares_below_rectangle'
|
||||
case 'oh-map-page':
|
||||
return 'map'
|
||||
default:
|
||||
return 'tv'
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// This component has no template, due to its special nature (has to add a tabbar to
|
||||
// the host page) it'll simply be handled in the page renderers themselves
|
||||
|
||||
export default {
|
||||
widget: {
|
||||
name: 'oh-tab',
|
||||
title: 'Tab',
|
||||
description: 'Displays a page in a tab',
|
||||
props: {
|
||||
parameters: [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
type: 'TEXT',
|
||||
description: 'The title of the tab'
|
||||
},
|
||||
{
|
||||
name: 'icon',
|
||||
label: 'Icon',
|
||||
type: 'TEXT',
|
||||
description: 'The icon on the tab: use <code>f7:iconName</code> (<a class="external text-color-blue" target="_blank" href="https://framework7.io/icons/">Framework7 icon</a>)'
|
||||
},
|
||||
{
|
||||
name: 'page',
|
||||
label: 'Page',
|
||||
type: 'TEXT',
|
||||
context: 'page',
|
||||
description: 'The page to display'
|
||||
},
|
||||
{
|
||||
name: 'pageConfig',
|
||||
label: 'Page Configuration',
|
||||
type: 'TEXT',
|
||||
context: 'props',
|
||||
description: 'The parameters (props) to configure the page, if any'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,456 @@
|
|||
<template>
|
||||
<f7-page @page:afterin="onPageAfterIn" @page:beforeout="onPageBeforeOut" class="tabs-editor">
|
||||
<f7-navbar :title="(!ready) ? '' : (createMode) ? 'Create tabbed page' : page.config.label" back-link="Back" no-hairline>
|
||||
<f7-nav-right>
|
||||
<f7-link @click="save()" v-if="$theme.md" icon-md="material:save" icon-only></f7-link>
|
||||
<f7-link @click="save()" v-if="!$theme.md">Save<span v-if="$device.desktop"> (Ctrl-S)</span></f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
<f7-toolbar tabbar position="top">
|
||||
<f7-link @click="currentTab = 'design'; fromYaml()" :tab-link-active="currentTab === 'design'" class="tab-link">Design</f7-link>
|
||||
<!-- <f7-link @click="currentTab = 'preview'" :tab-link-active="currentTab === 'preview'" class="tab-link">Preview</f7-link> -->
|
||||
<f7-link @click="currentTab = 'code'; toYaml()" :tab-link-active="currentTab === 'code'" class="tab-link">Code</f7-link>
|
||||
</f7-toolbar>
|
||||
<f7-toolbar bottom class="toolbar-details" v-show="currentTab === 'design'">
|
||||
<!-- <div style="margin-left: auto">
|
||||
<f7-toggle :checked="previewMode" @toggle:change="(value) => previewMode = value"></f7-toggle> Run mode<span v-if="$device.desktop"> (Ctrl-R)</span>
|
||||
</div> -->
|
||||
</f7-toolbar>
|
||||
<f7-tabs class="tabs-editor-tabs">
|
||||
<f7-tab id="design" class="tabs-editor-design-tab" @tab:show="() => this.currentTab = 'design'" :tab-active="currentTab === 'design'">
|
||||
<f7-block v-if="!ready" class="text-align-center">
|
||||
<f7-preloader></f7-preloader>
|
||||
<div>Loading...</div>
|
||||
</f7-block>
|
||||
<f7-block class="block-narrow" v-if="ready && !previewMode">
|
||||
<f7-col>
|
||||
<f7-list inline-labels>
|
||||
<f7-list-input label="ID" type="text" placeholder="ID" :value="page.uid" @input="page.uid = $event.target.value"
|
||||
required validate pattern="[A-Za-z0-9_]+" error-message="Required. Alphanumeric & underscores only" :disabled="!createMode">
|
||||
</f7-list-input>
|
||||
<f7-list-input label="Label" type="text" placeholder="Label" :value="page.config.label" @input="page.config.label = $event.target.value" clear-button>
|
||||
</f7-list-input>
|
||||
<f7-list-item title="Show on sidebar">
|
||||
<f7-toggle slot="after" :checked="page.config.sidebar" @toggle:change="page.config.sidebar = $event"></f7-toggle>
|
||||
</f7-list-item>
|
||||
<f7-list-input label="Sidebar order" type="number" placeholder="Assign order index to rearrange pages on sidebar" :value="page.config.order" @input="page.config.order = $event.target.value" clear-button>
|
||||
</f7-list-input>
|
||||
</f7-list>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
|
||||
<f7-block class="block-narrow" v-if="ready">
|
||||
<f7-col>
|
||||
<f7-block-title>Tabs</f7-block-title>
|
||||
<f7-menu v-if="clipboardType === 'oh-tab'">
|
||||
<f7-menu-item style="margin-left: auto" icon-f7="squares_below_rectangle" dropdown>
|
||||
<f7-menu-dropdown right>
|
||||
<f7-menu-dropdown-item @click="pasteWidget(page, null)" href="#" text="Paste"></f7-menu-dropdown-item>
|
||||
</f7-menu-dropdown>
|
||||
</f7-menu-item>
|
||||
</f7-menu>
|
||||
|
||||
<f7-list media-list>
|
||||
<f7-list-item media-item v-for="(tab, idx) in page.slots.default" :key="idx"
|
||||
:title="tab.config.title" :subtitle="tab.config.page">
|
||||
<f7-icon slot="media" :ios="tab.config.icon" :md="tab.config.icon" :aurora="tab.config.icon" color="gray" />
|
||||
<f7-menu slot="content-start">
|
||||
<f7-menu-item icon-f7="list_bullet" dropdown>
|
||||
<f7-menu-dropdown>
|
||||
<f7-menu-dropdown-item @click="configureWidget(tab, { component: page })" href="#" text="Configure Tab"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="editWidgetCode(tab, { component: page })" href="#" text="Edit YAML"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item divider></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="cutWidget(tab, { component: page })" href="#" text="Cut"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="copyWidget(tab, { component: page })" href="#" text="Copy"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item divider></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="moveWidgetUp(tab, { component: page })" href="#" text="Move Up"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="moveWidgetDown(tab, { component: page })" href="#" text="Move Down"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item divider></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item @click="removeWidget(tab, { component: page })" href="#" text="Remove Tab"></f7-menu-dropdown-item>
|
||||
</f7-menu-dropdown>
|
||||
</f7-menu-item>
|
||||
</f7-menu>
|
||||
</f7-list-item>
|
||||
<f7-list-button color="blue" title="Add tab" @click="addWidget(page, 'oh-tab')" />
|
||||
</f7-list>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
|
||||
</f7-tab>
|
||||
|
||||
<f7-tab id="preview" class="tabs-editor-preview-tab" @tab:show="() => this.currentTab = 'preview'" :tab-active="currentTab === 'preview'">
|
||||
|
||||
</f7-tab>
|
||||
<f7-tab id="code" @tab:show="() => { this.currentTab = 'code' }" :tab-active="currentTab === 'code'">
|
||||
<editor v-if="currentTab === 'code'" class="page-code-editor" mode="text/x-yaml" :value="pageYaml" @input="(value) => pageYaml = value" />
|
||||
<pre class="yaml-message padding-horizontal" :class="[yamlError === 'OK' ? 'text-color-green' : 'text-color-red']">{{yamlError}}</pre>
|
||||
</f7-tab>
|
||||
|
||||
</f7-tabs>
|
||||
|
||||
<f7-popup ref="widgetConfig" class="widgetconfig-popup" close-on-escape :opened="widgetConfigOpened" @popup:closed="widgetConfigClosed">
|
||||
<f7-page v-if="currentComponent && currentWidget">
|
||||
<f7-navbar>
|
||||
<f7-nav-left>
|
||||
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" popup-close></f7-link>
|
||||
</f7-nav-left>
|
||||
<f7-nav-title>Edit {{currentWidget.label || currentWidget.uid}}</f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link @click="updateWidgetConfig">Done</f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
<f7-block v-if="currentWidget.props">
|
||||
<f7-col>
|
||||
<config-sheet
|
||||
:parameterGroups="currentWidget.props.parameterGroups || []"
|
||||
:parameters="currentWidget.props.parameters || []"
|
||||
:configuration="currentComponentConfig"
|
||||
@updated="dirty = true"
|
||||
/>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
</f7-page>
|
||||
</f7-popup>
|
||||
|
||||
<f7-popup ref="widgetCode" class="widgetcode-popup" close-on-escape :opened="widgetCodeOpened" @popup:closed="widgetCodeClosed">
|
||||
<f7-page v-if="currentComponent && widgetCodeOpened">
|
||||
<f7-navbar>
|
||||
<f7-nav-left>
|
||||
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" popup-close></f7-link>
|
||||
</f7-nav-left>
|
||||
<f7-nav-title>Edit Widget Code</f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link @click="updateWidgetCode">Done</f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
<editor class="page-code-editor" mode="text/x-yaml" :value="widgetYaml" @input="(value) => widgetYaml = value" />
|
||||
<pre class="yaml-message padding-horizontal" :class="[widgetYamlError === 'OK' ? 'text-color-green' : 'text-color-red']">{{widgetYamlError}}</pre>
|
||||
</f7-page>
|
||||
</f7-popup>
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.sitemap-editor-tabs
|
||||
--f7-grid-gap 0px
|
||||
height calc(100% - var(--f7-toolbar-height))
|
||||
.tab
|
||||
height 100%
|
||||
.page-code-editor.vue-codemirror
|
||||
display block
|
||||
top calc(var(--f7-navbar-height) + var(--f7-tabbar-height))
|
||||
height calc(80% - 2*var(--f7-navbar-height))
|
||||
width 100%
|
||||
.yaml-message
|
||||
display block
|
||||
position absolute
|
||||
top 80%
|
||||
white-space pre-wrap
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import YAML from 'yaml'
|
||||
|
||||
// import OhMapPage from '@/components/widgets/map/oh-map-page.vue'
|
||||
import OhTab from './oh-tab'
|
||||
import ConfigSheet from '@/components/config/config-sheet.vue'
|
||||
|
||||
const ConfigurableWidgets = { OhTab }
|
||||
|
||||
function uuidv4 () {
|
||||
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'editor': () => import('@/components/config/controls/script-editor.vue'),
|
||||
ConfigSheet
|
||||
},
|
||||
props: ['createMode', 'uid'],
|
||||
data () {
|
||||
return {
|
||||
pageReady: false,
|
||||
loading: false,
|
||||
page: {
|
||||
uid: 'page_' + uuidv4().split('-')[0],
|
||||
component: 'oh-tabs-page',
|
||||
config: {},
|
||||
slots: { default: [] }
|
||||
},
|
||||
pageKey: uuidv4(),
|
||||
pageYaml: null,
|
||||
previewMode: false,
|
||||
currentTab: 'design',
|
||||
clipboard: null,
|
||||
clipboardType: null,
|
||||
currentComponent: null,
|
||||
currentComponentConfig: null,
|
||||
currentWidget: null,
|
||||
widgetConfigOpened: false,
|
||||
widgetCodeOpened: false,
|
||||
widgetYaml: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ready () {
|
||||
return this.pageReady && this.$store.state.components.widgets != null
|
||||
},
|
||||
context () {
|
||||
return {
|
||||
component: this.page,
|
||||
store: this.$store.getters.trackedItems,
|
||||
// states: this.stateTracking.store,
|
||||
editmode: (!this.previewMode) ? {
|
||||
addWidget: this.addWidget,
|
||||
configureWidget: this.configureWidget,
|
||||
editWidgetCode: this.editWidgetCode,
|
||||
cutWidget: this.cutWidget,
|
||||
copyWidget: this.copyWidget,
|
||||
pasteWidget: this.pasteWidget,
|
||||
moveWidgetUp: this.moveWidgetUp,
|
||||
moveWidgetDown: this.moveWidgetDown,
|
||||
removeWidget: this.removeWidget
|
||||
} : null,
|
||||
clipboardtype: this.clipboardType
|
||||
}
|
||||
},
|
||||
yamlError () {
|
||||
if (this.currentTab !== 'code') return null
|
||||
try {
|
||||
YAML.parse(this.pageYaml, { prettyErrors: true })
|
||||
return 'OK'
|
||||
} catch (e) {
|
||||
return e
|
||||
}
|
||||
},
|
||||
widgetYamlError () {
|
||||
if (!this.widgetCodeOpened) return null
|
||||
try {
|
||||
YAML.parse(this.widgetYaml, { prettyErrors: true })
|
||||
return 'OK'
|
||||
} catch (e) {
|
||||
return e
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onPageAfterIn () {
|
||||
if (window) {
|
||||
window.addEventListener('keydown', this.keyDown)
|
||||
}
|
||||
this.$store.dispatch('startTrackingStates')
|
||||
this.load()
|
||||
},
|
||||
onPageBeforeOut () {
|
||||
if (window) {
|
||||
window.removeEventListener('keydown', this.keyDown)
|
||||
}
|
||||
this.$store.dispatch('stopTrackingStates')
|
||||
},
|
||||
keyDown (ev) {
|
||||
if (ev.ctrlKey || ev.metakKey) {
|
||||
switch (ev.keyCode) {
|
||||
// case 82:
|
||||
// this.previewMode = !this.previewMode
|
||||
// ev.stopPropagation()
|
||||
// ev.preventDefault()
|
||||
// break
|
||||
case 83:
|
||||
this.save(!this.createMode)
|
||||
ev.stopPropagation()
|
||||
ev.preventDefault()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
load () {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
if (this.createMode) {
|
||||
this.loading = false
|
||||
this.pageReady = true
|
||||
} else {
|
||||
this.$oh.api.get('/rest/ui/components/ui:page/' + this.uid).then((data) => {
|
||||
this.$set(this, 'page', data)
|
||||
this.pageReady = true
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
save (stay) {
|
||||
if (!this.page.uid) {
|
||||
this.$f7.dialog.alert('Please give an ID to the page')
|
||||
return
|
||||
}
|
||||
if (!this.page.config.label) {
|
||||
this.$f7.dialog.alert('Please give an label to the page')
|
||||
return
|
||||
}
|
||||
if (!this.createMode && this.uid !== this.page.uid) {
|
||||
this.$f7.dialog.alert('You cannot change the ID of an existing page. Duplicate it with the new ID then delete this one.')
|
||||
return
|
||||
}
|
||||
|
||||
const promise = (this.createMode)
|
||||
? this.$oh.api.postPlain('/rest/ui/components/ui:page', JSON.stringify(this.page), 'text/plain', 'application/json')
|
||||
: this.$oh.api.put('/rest/ui/components/ui:page/' + this.page.uid, this.page)
|
||||
promise.then((data) => {
|
||||
if (this.createMode) {
|
||||
this.$f7.toast.create({
|
||||
text: 'Page created',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
this.load()
|
||||
} else {
|
||||
this.$f7.toast.create({
|
||||
text: 'Page updated',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
}
|
||||
this.$f7.emit('sidebarRefresh', null)
|
||||
if (!stay) this.$f7router.back()
|
||||
}).catch((err) => {
|
||||
this.$f7.toast.create({
|
||||
text: 'Error while saving page: ' + err,
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
})
|
||||
},
|
||||
addWidget (component, widgetType, parentContext, slot) {
|
||||
if (!slot) slot = 'default'
|
||||
if (!component.slots) component.slots = {}
|
||||
if (!component.slots[slot]) component.slots[slot] = []
|
||||
if (widgetType) {
|
||||
component.slots[slot].push({
|
||||
component: widgetType,
|
||||
config: {},
|
||||
slots: { default: [] }
|
||||
})
|
||||
this.forceUpdate()
|
||||
}
|
||||
},
|
||||
widgetConfigClosed () {
|
||||
this.currentComponent = null
|
||||
this.currentWidget = null
|
||||
this.widgetConfigOpened = false
|
||||
},
|
||||
updateWidgetConfig () {
|
||||
this.$set(this.currentComponent, 'config', this.currentComponentConfig)
|
||||
this.forceUpdate()
|
||||
this.widgetConfigClosed()
|
||||
},
|
||||
widgetCodeClosed () {
|
||||
this.currentComponent = null
|
||||
this.currentWidget = null
|
||||
this.widgetCodeOpened = false
|
||||
},
|
||||
updateWidgetCode () {
|
||||
const updatedWidget = YAML.parse(this.widgetYaml)
|
||||
this.$set(this.currentComponent, 'config', updatedWidget.config)
|
||||
this.$set(this.currentComponent, 'slots', updatedWidget.slots)
|
||||
this.forceUpdate()
|
||||
this.widgetCodeClosed()
|
||||
},
|
||||
configureWidget (component, parentContext, forceComponentType) {
|
||||
const componentType = forceComponentType || component.component
|
||||
this.currentComponent = null
|
||||
this.currentWidget = null
|
||||
let widgetDefinition
|
||||
if (componentType.indexOf('widget:') === 0) {
|
||||
this.currentWidget = this.$store.getters.widget(componentType.substring(7))
|
||||
} else {
|
||||
widgetDefinition = Object.values(ConfigurableWidgets).find((w) => w.widget && w.widget.name === componentType)
|
||||
if (!widgetDefinition) {
|
||||
// widgetDefinition = Object.values(LayoutWidgets).find((w) => w.widget.name === component.component)
|
||||
if (!widgetDefinition) {
|
||||
console.warn('Widget not found: ' + componentType)
|
||||
this.$f7.toast.create({
|
||||
text: `This type of component cannot be configured: ${componentType}.`,
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 3000,
|
||||
closeButton: true,
|
||||
closeButtonText: 'Edit YAML',
|
||||
on: {
|
||||
closeButtonClick: () => {
|
||||
this.editWidgetCode(component, parentContext)
|
||||
}
|
||||
}
|
||||
}).open()
|
||||
return
|
||||
}
|
||||
}
|
||||
this.currentWidget = widgetDefinition.widget
|
||||
}
|
||||
this.currentComponent = component
|
||||
this.currentComponentConfig = JSON.parse(JSON.stringify(this.currentComponent.config))
|
||||
this.widgetConfigOpened = true
|
||||
},
|
||||
editWidgetCode (component, parentContext, slot) {
|
||||
if (slot && !component.slots) component.slots = {}
|
||||
if (slot && !component.slots[slot]) component.slots[slot] = []
|
||||
this.currentComponent = component
|
||||
this.widgetYaml = YAML.stringify(component)
|
||||
this.widgetCodeOpened = true
|
||||
},
|
||||
cutWidget (component, parentContext) {
|
||||
this.copyWidget(component, parentContext)
|
||||
this.removeWidget(component, parentContext)
|
||||
},
|
||||
copyWidget (component, parentContext) {
|
||||
let newClipboard = JSON.stringify(component)
|
||||
this.$set(this, 'clipboard', newClipboard)
|
||||
this.clipboardType = component.component
|
||||
},
|
||||
pasteWidget (component, parentContext) {
|
||||
if (!this.clipboard) return
|
||||
component.slots.default.push(JSON.parse(this.clipboard))
|
||||
this.forceUpdate()
|
||||
},
|
||||
moveWidgetUp (component, parentContext) {
|
||||
let siblings = parentContext.component.slots.default
|
||||
let pos = siblings.indexOf(component)
|
||||
if (pos <= 0) return
|
||||
siblings.splice(pos, 1)
|
||||
siblings.splice(pos - 1, 0, component)
|
||||
this.forceUpdate()
|
||||
},
|
||||
moveWidgetDown (component, parentContext) {
|
||||
let siblings = parentContext.component.slots.default
|
||||
let pos = siblings.indexOf(component)
|
||||
if (pos >= siblings.length - 1) return
|
||||
siblings.splice(pos, 1)
|
||||
siblings.splice(pos + 1, 0, component)
|
||||
this.forceUpdate()
|
||||
},
|
||||
removeWidget (component, parentContext) {
|
||||
parentContext.component.slots.default.splice(parentContext.component.slots.default.indexOf(component), 1)
|
||||
this.forceUpdate()
|
||||
},
|
||||
forceUpdate () {
|
||||
this.pageKey = uuidv4()
|
||||
},
|
||||
toYaml () {
|
||||
this.pageYaml = YAML.stringify({
|
||||
tabs: this.page.slots.default
|
||||
})
|
||||
},
|
||||
fromYaml () {
|
||||
try {
|
||||
const updatedTabs = YAML.parse(this.pageYaml)
|
||||
this.$set(this.page.slots, 'default', updatedTabs.tabs)
|
||||
this.forceUpdate()
|
||||
return true
|
||||
} catch (e) {
|
||||
this.$f7.dialog.alert(e).open()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
Loading…
Reference in New Issue