tailbone/tailbone/templates/configure-menus.mako
Lance Edgar 1ec1eba496 feat: refactor templates to simplify base/page/form structure
to mimic what has been done in wuttaweb
2024-08-19 23:20:59 -05:00

446 lines
13 KiB
Mako

## -*- coding: utf-8; -*-
<%inherit file="/configure.mako" />
<%def name="extra_styles()">
${parent.extra_styles()}
<style type="text/css">
.topmenu-dropper {
min-width: 0.8rem;
}
.topmenu-dropper:-moz-drag-over {
background-color: blue;
}
</style>
</%def>
<%def name="form_content()">
## nb. must be root to configure menus! otherwise some of the
## currently-defined menus may not appear on the page, so saving
## would inadvertently remove them!
% if request.is_root:
${h.hidden('menus', **{':value': 'JSON.stringify(allMenuData)'})}
<h3 class="is-size-3">Top-Level Menus</h3>
<p class="block">Click on a menu to edit.&nbsp; Drag things around to rearrange.</p>
<b-field grouped>
<b-field grouped v-for="key in menuSequence"
:key="key">
<span class="topmenu-dropper control"
@dragover.prevent
@dragenter.prevent
@drop="dropMenu($event, key)">
&nbsp;
</span>
<b-button :type="editingMenu && editingMenu.key == key ? 'is-primary' : null"
class="control"
@click="editMenu(key)"
:disabled="editingMenu && editingMenu.key != key"
:draggable="!editingMenu"
@dragstart.native="topMenuStartDrag($event, key)">
{{ allMenus[key].title }}
</b-button>
</b-field>
<div class="topmenu-dropper control"
@dragover.prevent
@dragenter.prevent
@drop="dropMenu($event, '_last_')">
&nbsp;
</div>
<b-button v-show="!editingMenu"
type="is-primary"
icon-pack="fas"
icon-left="plus"
@click="editMenuNew()">
Add
</b-button>
</b-field>
<div v-if="editingMenu"
style="max-width: 40%;">
<b-field grouped>
<b-field label="Label">
<b-input v-model="editingMenu.title"
ref="editingMenuTitleInput">
</b-input>
</b-field>
<b-field label="Actions">
<div class="buttons">
<b-button icon-pack="fas"
icon-left="redo"
@click="editMenuCancel()">
Revert / Cancel
</b-button>
<b-button type="is-primary"
icon-pack="fas"
icon-left="save"
@click="editMenuSave()">
Save
</b-button>
<b-button type="is-danger"
icon-pack="fas"
icon-left="trash"
@click="editMenuDelete()">
Delete
</b-button>
</div>
</b-field>
</b-field>
<b-field>
<template #label>
<span style="margin-right: 2rem;">Menu Items</span>
<b-button type="is-primary"
icon-pack="fas"
icon-left="plus"
@click="editMenuItemInitDialog()">
Add
</b-button>
</template>
<ul class="list">
<li v-for="item in editingMenu.items"
class="list-item"
draggable
@dragstart="menuItemStartDrag($event, item)"
@dragover.prevent
@dragenter.prevent
@drop="menuItemDrop($event, item)">
<span :class="item.type == 'sep' ? 'has-text-info' : null">
{{ item.type == 'sep' ? "-- separator --" : item.title }}
</span>
<span class="is-pulled-right grid-action">
<a href="#" @click.prevent="editMenuItemInitDialog(item)">
<i class="fas fa-edit"></i>
Edit
</a>
&nbsp;
<a href="#" class="has-text-danger"
@click.prevent="editMenuItemDelete(item)">
<i class="fas fa-trash"></i>
Delete
</a>
&nbsp;
</span>
</li>
</ul>
</b-field>
<b-modal has-modal-card
:active.sync="editMenuItemShowDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{{ editingMenuItem.isNew ? "Add" : "Edit" }} Item</p>
</header>
<section class="modal-card-body">
<b-field label="Item Type">
<b-select v-model="editingMenuItem.type">
<option value="item">Route Link</option>
<option value="sep">Separator</option>
</b-select>
</b-field>
<b-field label="Route"
v-show="editingMenuItem.type == 'item'">
<b-select v-model="editingMenuItem.route"
@input="editingMenuItemRouteChanged">
<option v-for="route in editMenuIndexRoutes"
:key="route.route"
:value="route.route">
{{ route.label }}
</option>
</b-select>
</b-field>
<b-field label="Label"
v-show="editingMenuItem.type == 'item'">
<b-input v-model="editingMenuItem.title">
</b-input>
</b-field>
</section>
<footer class="modal-card-foot">
<b-button @click="editMenuItemShowDialog = false">
Cancel
</b-button>
<b-button type="is-primary"
icon-pack="fas"
icon-left="save"
:disabled="editMenuItemSaveDisabled"
@click="editMenuSaveItem()">
Save
</b-button>
</footer>
</div>
</b-modal>
</div>
% else:
## not root!
<b-notification type="is-warning">
You must become root to configure menus!
</b-notification>
% endif
</%def>
## TODO: should probably make some global "editable" flag that the
## base configure template has knowledge of, and just set that to
## false for this view
<%def name="purge_button()">
% if request.is_root:
${parent.purge_button()}
% endif
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.menuSequence = ${json.dumps([m['key'] for m in menus])|n}
ThisPageData.allMenus = {}
% for topitem in menus:
ThisPageData.allMenus['${topitem['key']}'] = ${json.dumps(topitem)|n}
% endfor
ThisPageData.editMenuIndexRoutes = ${json.dumps(index_route_options)|n}
ThisPageData.editingMenu = null
ThisPageData.editingMenuItem = {isNew: true}
ThisPageData.editingMenuItemIndex = null
ThisPageData.editMenuItemShowDialog = false
// nb. this value is sent on form submit
ThisPage.computed.allMenuData = function() {
let menus = []
for (key of this.menuSequence) {
menus.push(this.allMenus[key])
}
return menus
}
ThisPage.methods.editMenu = function(key) {
if (this.editingMenu) {
return
}
// copy existing (original) menu to be edited
let original = this.allMenus[key]
this.editingMenu = {
key: key,
title: original.title,
items: [],
}
// and copy each item separately
for (let item of original.items) {
this.editingMenu.items.push({
key: item.key,
title: item.title,
route: item.route,
url: item.url,
perm: item.perm,
type: item.type,
})
}
}
ThisPage.methods.editMenuNew = function() {
// editing brand new menu
this.editingMenu = {items: []}
// focus title input
this.$nextTick(() => {
this.$refs.editingMenuTitleInput.focus()
})
}
ThisPage.methods.editMenuCancel = function(key) {
this.editingMenu = null
}
ThisPage.methods.editMenuSave = function() {
let key = this.editingMenu.key
if (key) {
// update existing (original) menu with user edits
this.allMenus[key] = this.editingMenu
} else {
// generate makeshift key
key = this.editingMenu.title.replace(/\W/g, '')
// add new menu to data set
this.allMenus[key] = this.editingMenu
this.menuSequence.push(key)
}
// no longer editing
this.editingMenu = null
this.settingsNeedSaved = true
}
ThisPage.methods.editMenuDelete = function() {
if (confirm("Really delete this menu?")) {
let key = this.editingMenu.key
// remove references from primary collections
let i = this.menuSequence.indexOf(key)
this.menuSequence.splice(i, 1)
delete this.allMenus[key]
// no longer editing
this.editingMenu = null
this.settingsNeedSaved = true
}
}
## TODO: see also https://learnvue.co/2020/01/how-to-add-drag-and-drop-to-your-vuejs-project/#adding-drag-and-drop-functionality
## TODO: see also https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API
## TODO: maybe try out https://www.npmjs.com/package/vue-drag-drop
ThisPage.methods.topMenuStartDrag = function(event, key) {
event.dataTransfer.setData('key', key)
}
ThisPage.methods.dropMenu = function(event, target) {
let key = event.dataTransfer.getData('key')
if (target == key) {
return // same target
}
let i = this.menuSequence.indexOf(key)
let j = this.menuSequence.indexOf(target)
if (i + 1 == j) {
return // same target
}
if (target == '_last_') {
if (this.menuSequence[this.menuSequence.length-1] != key) {
this.menuSequence.splice(i, 1)
this.menuSequence.push(key)
this.settingsNeedSaved = true
}
} else {
this.menuSequence.splice(i, 1)
j = this.menuSequence.indexOf(target)
this.menuSequence.splice(j, 0, key)
this.settingsNeedSaved = true
}
}
ThisPage.methods.menuItemStartDrag = function(event, item) {
let i = this.editingMenu.items.indexOf(item)
event.dataTransfer.setData('itemIndex', i)
}
ThisPage.methods.menuItemDrop = function(event, item) {
let oldIndex = event.dataTransfer.getData('itemIndex')
let pruned = this.editingMenu.items.splice(oldIndex, 1)
let newIndex = this.editingMenu.items.indexOf(item)
this.editingMenu.items.splice(newIndex, 0, pruned[0])
}
ThisPage.methods.editMenuItemInitDialog = function(item) {
if (item === undefined) {
this.editingMenuItemIndex = null
// create new item to edit
this.editingMenuItem = {
isNew: true,
route: null,
title: null,
perm: null,
type: 'item',
}
} else {
this.editingMenuItemIndex = this.editingMenu.items.indexOf(item)
// copy existing (original item to be edited
this.editingMenuItem = {
key: item.key,
title: item.title,
route: item.route,
url: item.url,
perm: item.perm,
type: item.type,
}
}
this.editMenuItemShowDialog = true
}
ThisPage.methods.editingMenuItemRouteChanged = function(routeName) {
for (let route of this.editMenuIndexRoutes) {
if (route.route == routeName) {
this.editingMenuItem.title = route.label
this.editingMenuItem.perm = route.perm
break
}
}
}
ThisPage.computed.editMenuItemSaveDisabled = function() {
if (this.editingMenuItem.type == 'item') {
if (!this.editingMenuItem.route) {
return true
}
if (!this.editingMenuItem.title) {
return true
}
}
return false
}
ThisPage.methods.editMenuSaveItem = function() {
if (this.editingMenuItem.isNew) {
this.editingMenu.items.push(this.editingMenuItem)
} else {
this.editingMenu.items.splice(this.editingMenuItemIndex,
1,
this.editingMenuItem)
}
this.editMenuItemShowDialog = false
}
ThisPage.methods.editMenuItemDelete = function(item) {
if (confirm("Really delete this item?")) {
// remove item from editing menu
let i = this.editingMenu.items.indexOf(item)
this.editingMenu.items.splice(i, 1)
}
}
</script>
</%def>