first draft of link picker overlay

This commit is contained in:
2021-01-30 16:52:36 +01:00
parent 77f9ea63c4
commit d21f5501d7
18 changed files with 396 additions and 64 deletions
+18
View File
@@ -0,0 +1,18 @@
using Raven.Client.Documents.Session;
using System.Collections.Generic;
using System.Threading.Tasks;
using zero.Core.Entities;
namespace zero.Core.Routing
{
public interface ILinkListProvider : ILinkProvider
{
/// <summary>
/// Get paged list items.
/// <param name="session">Current document session</param>
/// <param name="page">Current page number (one-based)</param>
/// <param name="search">Search query</param>
/// </summary>
Task<IList<TreeItem>> GetLinkListItems(IAsyncDocumentSession session, int page = 1, string search = null);
}
}
+18
View File
@@ -0,0 +1,18 @@
using Raven.Client.Documents.Session;
using System.Collections.Generic;
using System.Threading.Tasks;
using zero.Core.Entities;
namespace zero.Core.Routing
{
public interface ILinkTreeProvider : ILinkProvider
{
/// <summary>
/// Get tree children for the current parent id.
/// <param name="session">Current document session</param>
/// <param name="parentId">Parent node id</param>
/// <param name="activeId">Selected node so parents can be set to open for the tree to load correclty</param>
/// </summary>
Task<IList<TreeItem>> GetLinkTreeItems(IAsyncDocumentSession session, string parentId = null, string activeId = null);
}
}
+7 -14
View File
@@ -15,10 +15,7 @@
props: {
value: [String, Number, Object],
items: [Array, Function],
entity: {
type: Object,
required: true
},
entity: Object,
disabled: Boolean,
emptyOption: {
type: Boolean,
@@ -57,22 +54,18 @@
methods: {
rebuild()
{
let items = [];
if (!this.entity || !this.items)
{
this.options = items;
return;
}
if (typeof this.items === 'function')
if (this.entity && typeof this.items === 'function')
{
this.options = this.items(this.entity);
}
else
else if (typeof this.items !== 'function' && this.items)
{
this.options = [...this.items];
}
else
{
this.options = [];
}
},
onChange(ev)
@@ -18,7 +18,7 @@
import { extend as _extend, isArray as _isArray, isEmpty as _isEmpty, clone as _clone } from 'underscore';
export default {
name: 'uiPagepicker',
name: 'uiLinkpicker',
props: {
value: {
@@ -29,6 +29,26 @@
type: Number,
default: 1
},
title: {
type: Boolean,
default: true
},
label: {
type: Boolean,
default: false
},
target: {
type: Boolean,
default: true
},
suffix: {
type: Boolean,
default: false
},
areas: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
@@ -121,7 +141,15 @@
closeLabel: '@ui.close',
component: LinkpickerOverlay,
display: 'editor',
model: this.multiple ? id : this.value
model: this.multiple ? id : this.value,
options: {
limit: this.limit,
label: this.label,
title: this.title,
target: this.target,
suffix: this.suffix,
areas: this.areas
}
}, typeof this.options === 'object' ? this.options : {});
return Overlay.open(options).then(value =>
@@ -1,15 +1,45 @@
<template>
<ui-overlay-editor class="ui-pagepicker-overlay">
<ui-overlay-editor class="ui-linkpicker-overlay">
<template v-slot:header>
<ui-header-bar :title="config.title" :back-button="false" :close-button="true" />
</template>
<template v-slot:footer>
<ui-button type="light onbg" :label="config.closeLabel" :parent="config.rootId" @click="config.hide"></ui-button>
<ui-button type="light onbg" :label="config.closeLabel" :parent="config.rootId" @click="config.hide" />
</template>
<div v-if="opened" class="ui-box ui-pagepicker-overlay-items">
<ui-tree ref="tree" :get="getItems" :parent="config.rootId" @select="onSelect" />
<div v-if="opened">
<div class="ui-box ui-linkpicker-overlay-options">
<ui-property label="Open in a new tab">
<ui-toggle :value="link.target === 'blank'" @input="onTargetChange" />
</ui-property>
<template v-if="showOptions">
<hr />
<ui-property label="Label" :vertical="true">
<input v-model="link.label" type="text" class="ui-input" maxlength="160" />
</ui-property>
<ui-property label="Title" :vertical="true">
<input v-model="link.title" type="text" class="ui-input" maxlength="160" />
</ui-property>
<hr />
<ui-button label="Hide options" @click="showOptions=false" />
</template>
<div v-if="!showOptions">
<hr />
<ui-button label="More options" @click="showOptions=true" caret="down" />
</div>
</div>
<div class="ui-box">
<ui-property label="Area" :vertical="true">
<ui-select v-model="current" :items="areaItems"></ui-select>
</ui-property>
<div class="ui-linkpicker-overlay-items" v-if="area">
<ui-tree v-if="area.display === 'tree'" ref="tree" v-bind="treeConfig" :get="getTreeItems" @select="treeConfig.onSelect" />
</div>
</div>
</div>
<!--<div v-if="opened" class="ui-box ui-linkpicker-overlay-items">
<ui-tree ref="tree" :get="getItems" :parent="config.rootId" @select="onSelect" />
</div>-->
</ui-overlay-editor>
</template>
@@ -25,34 +55,100 @@
},
data: () => ({
opened: false
opened: false,
current: null,
area: null,
areas: [],
areaItems: [],
treeConfig: {
parent: null,
active: null,
onSelect: (ev) => { console.info(ev); }
},
link: null,
template: {
area: null,
target: 'default',
urlSuffix: null,
label: null,
title: null,
values: {}
},
showOptions: false
}),
computed: {
disabledIds()
watch: {
current()
{
return this.config.disabledIds || [];
this.reloadSelector();
}
},
mounted()
{
this.areas = this.zero.config.linkPicker.areas;
this.areaItems = this.areas.map(x =>
{
return {
key: x.alias,
value: x.name
};
});
this.area = this.areas[0];
this.current = this.area.alias;
this.link = JSON.parse(JSON.stringify(this.template));
this.link.area = this.current;
setTimeout(() => this.opened = true, 300);
},
methods: {
reloadSelector()
{
this.area = this.areas.find(x => x.alias === this.current);
if (!this.opened)
{
return;
}
if (this.area.display === 'tree' && this.$refs.tree)
{
this.$refs.tree.refresh();
}
else if (this.area.display === 'list')
{
}
else if (this.area.display === 'media')
{
}
else
{
// custom
}
},
onSelect(item)
{
this.config.confirm(item);
},
// get tree items
getItems(parent)
// get list items
getListItems(search)
{
return PageTreeApi.getChildren(parent, this.model).then(res =>
},
// get tree items
getTreeItems(parent)
{
return PageTreeApi.getChildren(parent, null).then(res =>
{
res = res.filter(x => x.id !== 'recyclebin');
@@ -62,55 +158,73 @@
{
item.isSelected = true;
}
if (this.disabledIds.indexOf(item.id) > -1)
{
item.disabled = true;
}
item.hasActions = false;
});
return res;
});
},
onTargetChange(ev)
{
this.link.target = ev ? 'blank' : 'default';
}
}
}
</script>
<style lang="scss">
.ui-pagepicker-overlay content
.ui-linkpicker-overlay content
{
padding-top: 0;
padding-top: 0;
}
.ui-box.ui-pagepicker-overlay-items
.ui-linkpicker-overlay-options .ui-property
{
margin: 0;
padding: 20px 0;
display: flex;
justify-content: space-between;
}
.ui-tree-item.is-selected, .ui-tree-item:hover:not(.is-disabled)
.ui-linkpicker-overlay-options .ui-property + .ui-property
{
margin-top: var(--padding-m);
}
.ui-linkpicker-overlay-options .ui-property-content
{
display: inline;
flex: 0 0 auto;
}
.ui-linkpicker-overlay-options .ui-property-label
{
padding-top: 1px;
}
.ui-linkpicker-overlay-items > .ui-tree
{
margin: 24px -32px 0;
}
.ui-linkpicker-overlay-items .ui-tree-item.is-selected, .ui-linkpicker-overlay-items .ui-tree-item:hover:not(.is-disabled)
{
background: var(--color-tree-selected);
}
/*.ui-linkpicker-overlay-items .ui-tree-item.is-selected
{
&:after
{
background: var(--color-bg-xxlight);
font-family: "Feather";
content: "\e83e";
font-size: 16px;
color: var(--color-primary);
}
& +.ui-box
{
margin-top: var(--padding);
}
.ui-tree-item.is-selected
{
&:after
{
font-family: "Feather";
content: "\e83e";
font-size: 16px;
color: var(--color-primary);
}
.ui-tree-item-text
{
font-weight: bold;
}
.ui-tree-item-text
{
font-weight: bold;
}
}
}*/
</style>
+1 -1
View File
@@ -149,7 +149,7 @@
color: var(--color-text);
background: var(--color-box);
box-shadow: var(--shadow-short);
border-top: 3px solid var(--color-primary);
//border-top: 3px solid var(--color-primary);
.ui-tabs-list-item-count
{
+1
View File
@@ -98,6 +98,7 @@
})
.catch(error =>
{
console.error(error);
this.setStatus('error', this.items, error);
// TODO handle errors
});
+32
View File
@@ -0,0 +1,32 @@
//import MediaApi from 'zero/api/media.js';
//import Strings from 'zero/helpers/strings.js';
//import Localization from 'zero/helpers/localization.js';
//class LinkProvider
//{
// key;
// label;
// icon;
// autoclose = true;
// action = () =>
// {
// console.warn(`[zero] A list action needs a "action" callback`);
// };
// constructor(alias: string, name: string, type: string)
// {
// this.key = key;
// this.label = label;
// this.icon = icon;
// this.action = action;
// }
// /**
// * Calls the action
// * @returns {ListColumn}
// */
// call(options)
// {
// console.info(options);
// this.action(options);
// }
//}
//export default ListAction;
//# sourceMappingURL=link-provider.js.map
@@ -0,0 +1 @@
{"version":3,"file":"link-provider.js","sourceRoot":"","sources":["link-provider.ts"],"names":[],"mappings":"AACA,2CAA2C;AAC3C,gDAAgD;AAChD,0DAA0D;AAE1D,oBAAoB;AACpB,GAAG;AACH,QAAQ;AACR,UAAU;AACV,SAAS;AACT,qBAAqB;AACrB,kBAAkB;AAClB,KAAK;AACL,qEAAqE;AACrE,MAAM;AAEN,0DAA0D;AAC1D,KAAK;AACL,qBAAqB;AACrB,yBAAyB;AACzB,uBAAuB;AACvB,2BAA2B;AAC3B,KAAK;AAEL,OAAO;AACP,uBAAuB;AACvB,4BAA4B;AAC5B,OAAO;AACP,iBAAiB;AACjB,KAAK;AACL,4BAA4B;AAC5B,2BAA2B;AAC3B,KAAK;AACL,GAAG;AAGH,4BAA4B"}
+37
View File
@@ -0,0 +1,37 @@
//import MediaApi from 'zero/api/media.js';
//import Strings from 'zero/helpers/strings.js';
//import Localization from 'zero/helpers/localization.js';
//class LinkProvider
//{
// key;
// label;
// icon;
// autoclose = true;
// action = () =>
// {
// console.warn(`[zero] A list action needs a "action" callback`);
// };
// constructor(alias: string, name: string, type: string)
// {
// this.key = key;
// this.label = label;
// this.icon = icon;
// this.action = action;
// }
// /**
// * Calls the action
// * @returns {ListColumn}
// */
// call(options)
// {
// console.info(options);
// this.action(options);
// }
//}
//export default ListAction;
+15
View File
@@ -12,5 +12,20 @@ export default {
images_natural: ['.jpg', '.jpeg', '.webp'],
images_artificial: ['.png', '.webp', '.svg']
}
},
linkPicker: {
areas: [
{
alias: 'zero.pages',
name: '@zero.config.linkareas.pages',
display: 'tree'
},
{
alias: 'zero.media',
name: '@zero.config.linkareas.media',
display: 'media'
}
]
}
}
+21 -1
View File
@@ -1,5 +1,5 @@
<template>
<ui-linkpicker :config="config" :value="value" @input="$emit('input', $event)" :disabled="disabled" :limit="limit" />
<ui-linkpicker :config="config" :value="value" @input="$emit('input', $event)" :disabled="disabled" v-bind="{ disabled, limit, title, label, target, suffix, areas }" />
</template>
@@ -18,6 +18,26 @@
type: Number,
default: 1
},
title: {
type: Boolean,
default: true
},
label: {
type: Boolean,
default: false
},
target: {
type: Boolean,
default: true
},
suffix: {
type: Boolean,
default: false
},
areas: {
type: Array,
default: () => []
},
config: Object
},
}
+1 -1
View File
@@ -7,5 +7,5 @@
--padding-l: 64px;
--height-top: 74px;
--radius: 3px;
--radius: 4px;
}
+5
View File
@@ -411,6 +411,11 @@ class EditorField
* Render a link picker
* @param {object} [options] - Custom options
* @param {number} [options.limit=1] - Limit of selection
* @param {boolean} [options.title=true] - Allow input of custom link title
* @param {boolean} [options.target=true] - Allow selection of the link target
* @param {boolean} [options.label=false] - Allow input of a custom label for button generation
* @param {boolean} [options.suffix=false] - Allow input of custom link URL suffix (query or hash)
* @param {string[]} [options.areas] - Limit link areas to the specified values (built-in are zero.pages, zero.media and zero.url)
* @returns {EditorField}
*/
linkPicker(options)
+12 -2
View File
@@ -7,6 +7,7 @@ class Plugin
routes = [];
editors = [];
lists = [];
linkAreas = [];
constructor(name)
@@ -54,9 +55,9 @@ class Plugin
/*
* Add a new vue list renderer to the global configuration
*/
addList(config)
addList(list)
{
this.lists.push(config);
this.lists.push(list);
}
@@ -87,6 +88,15 @@ class Plugin
{
routes.forEach(route => this.addRoute(route));
}
/*
* Add a new link area
*/
addLinkArea(area)
{
this.linkAreas.push(area);
}
};
export default Plugin;
+2
View File
@@ -128,6 +128,8 @@ class Zero
// append lists
plugin.lists.forEach(x => this.addOrReplace(this.#lists, x, 'alias'));
// append
console.log(`[zero] Installed %c${plugin.name}%cplugin`, 'font-style:italic;');
}
@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;
using zero.Core.Api;
using zero.Core.Entities;
namespace zero.Web.Controllers
{
public class LinkPickerController : BackofficeController
{
IPageTreeApi Api;
IPagesApi PagesApi;
public LinkPickerController(IPageTreeApi api, IPagesApi pagesApi)
{
Api = api;
PagesApi = pagesApi;
}
public async Task<IList<TreeItem>> GetChildren([FromQuery] string areaAlias, [FromQuery] string parent = null, [FromQuery] string active = null)
{
return await Api.GetChildren(parent, active);
}
//public async Task<IList<PreviewModel>> GetPreviews([FromQuery] List<string> ids)
//{
// return Previews(await PagesApi.GetByIds(ids.ToArray()), PreviewTransform);
//}
}
}
@@ -1,6 +1,13 @@
{
"zero": {
"name": "zero"
"name": "zero",
"config": {
"linkareas": {
"pages": "Pages",
"media": "Media"
}
}
},
"ui": {