This commit is contained in:
2021-12-07 15:59:29 +01:00
parent 58ba312e90
commit 6faa389feb
67 changed files with 950 additions and 893 deletions
+2
View File
@@ -54,6 +54,7 @@ public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiCont
protected async Task<ActionResult<Paged>> GetModels<T>(ListQuery<TModel> query) where T : BasicModel<TModel>
{
query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate);
query.SearchSelector ??= x => x.Name;
Paged<TModel> result = await Store.Load(query.Page, query.PageSize, q => q.Filter(query));
return Mapper.Map<TModel, T>(result, (src, dest) =>
@@ -66,6 +67,7 @@ public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiCont
protected async Task<ActionResult<Paged>> GetModels<T, TIndex>(ListQuery<TModel> query) where T : BasicModel<TModel> where TIndex : AbstractCommonApiForIndexes, new()
{
query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate);
query.SearchSelector ??= x => x.Name;
Paged<TModel> result = await Store.Load<TIndex>(query.Page, query.PageSize, q => q.Filter(query));
return Mapper.Map<TModel, T>(result, (src, dest) =>
+15
View File
@@ -0,0 +1,15 @@
<template>
<div class="root">
about
</div>
</template>
<script lang="ts">
export default {
data: () => ({
})
}
</script>
+15
View File
@@ -0,0 +1,15 @@
<template>
<div class="root">
this is the root
</div>
</template>
<script lang="ts">
export default {
data: () => ({
})
}
</script>
+42 -7
View File
@@ -1,6 +1,14 @@
<template>
<div class="app">
<div>
<div class="ui-box" style="width: 600px">
nav:
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-link to="/countries">Countries</router-link><br />
view:
<router-view></router-view>
</div>
<div class="ui-box">
<input type="text" v-model="input" />
<br />
output: {{output}}
@@ -24,6 +32,28 @@
Our settings <button type="button" @click="tabLabel = 'New Settings'">change label</button>
</ui-tab>
</ui-tabs>
<br /><br /><br />
buttons:
<ui-button label="Click" type="accent"></ui-button>
<ui-icon-button icon="fth-home" type="accent"></ui-icon-button>
<ui-dot-button></ui-dot-button>
<ui-select-button label="Choose" description="Select an item"></ui-select-button>
<ui-state-button :items="[{value: 'yes', label: 'Yes'}, {value: 'no', label: 'No'}]"></ui-state-button>
<br />
message: <ui-message text="Warning, this is not acceptable" />
<br />
pagination: <ui-pagination :pages="10" :page="2"></ui-pagination>
<br />
rte:
<br />
<ui-rte v-model="rte" />
<br />
<ui-toggle v-model="toggled" />
<br />
<ui-search v-if="toggled" v-model="search" @submit="searchSubmit($event)" />
<br />
countrypicker:
<ui-countrypicker v-if="!toggled" />
</div>
</div>
</template>
@@ -38,19 +68,24 @@
data: () => ({
input: null,
name: localize('@name'),
tabLabel: 'Settings'
tabLabel: 'Settings',
rte: '<p><b>Hallo</b><br>Das ist <i>schön</i></p><p>Ciao Tobi</p>',
toggled: true,
search: null
}),
created()
{
console.info(this.zero.version);
},
computed: {
output()
{
return selectorToArray(this.input);
}
},
methods: {
searchSubmit(ev)
{
console.info('searched: ' + ev);
}
}
}
@@ -1,13 +1,13 @@
import Axios from 'axios';
import Auth from 'zero/helpers/auth.js';
//import Auth from 'zero/helpers/auth.js';
import Qs from 'qs';
if (!__zero || !__zero.apiPath)
{
throw Exception('window.zero and zero.apiPath (= base path to the backoffice API) have to be configured');
}
//if (!__zero || !__zero.apiPath)
//{
// throw Exception('window.zero and zero.apiPath (= base path to the backoffice API) have to be configured');
//}
Axios.defaults.baseURL = __zero.apiPath;
Axios.defaults.baseURL = '/zero/api/';
Axios.defaults.withCredentials = true;
Axios.defaults.paramsSerializer = (params) =>
@@ -21,7 +21,8 @@ Axios.interceptors.response.use(response => response, error =>
{
if (error.response.status === 401)
{
Auth.rejectUser("@login.rejectReasons.inactive");
console.error('[zero.axios] Auth failed. Please login again.');
//Auth.rejectUser("@login.rejectReasons.inactive");
//Notification.error('Authentication failed. Please login again.', 3);
}
}
@@ -1,7 +0,0 @@
import uiIcon from './ui-icon.vue';
import uiLoading from './ui-loading.vue';
export {
uiIcon,
uiLoading
};
@@ -0,0 +1,29 @@
import uiIcon from './ui-icon.vue';
import uiLoading from './ui-loading.vue';
import uiTab from './ui-tab.vue';
import uiTabs from './ui-tabs.vue';
import uiInlineTabs from './ui-inline-tabs.vue';
import uiButton from './ui-button.vue';
import uiDotButton from './ui-dot-button.vue';
import uiIconButton from './ui-icon-button.vue';
import uiSelectButton from './ui-select-button.vue';
import uiStateButton from './ui-state-button.vue';
import uiMessage from './ui-message.vue';
import uiPagination from './ui-pagination.vue';
import uiError from './ui-error.vue';
export {
uiIcon,
uiLoading,
uiTab,
uiTabs,
uiInlineTabs,
uiButton,
uiDotButton,
uiIconButton,
uiSelectButton,
uiStateButton,
uiMessage,
uiPagination,
uiError
};
@@ -1,10 +1,13 @@
import { App } from 'vue';
import registerGeneric from './generic/register';
import registerTabs from './tabs/register';
import * as components from './index';
export default function (app: App)
{
registerGeneric(app);
registerTabs(app);
for (var key in components)
{
let component = components[key];
app.component(key, component.default || component);
}
};
@@ -1,10 +0,0 @@
import uiTab from './ui-tab.vue';
import uiTabs from './ui-tabs.vue';
import uiInlineTabs from './ui-inline-tabs.vue';
export
{
uiTab,
uiTabs,
uiInlineTabs
};
@@ -1,13 +0,0 @@
import { App } from 'vue';
import * as components from './index';
export default function (app: App)
{
for (var key in components)
{
let component = components[key];
app.component(key, component.default || component);
}
};
@@ -1,5 +1,5 @@
<template>
<button :type="buttonType" class="ui-button has-state" :class="buttonClass" :disabled="disabled || state == 'loading'" @click="tryClick">
<button :type="buttonType" class="ui-button has-state" :class="buttonClass" :disabled="disabled || state == 'loading' || !isDefaultState">
<span v-if="label" class="ui-button-text" v-localize:html="label"></span>
<ui-icon v-if="caret" :symbol="caretSymbol" class="ui-button-caret" />
<ui-icon v-if="icon" :symbol="icon" class="ui-button-icon" :stroke="stroke" />
@@ -149,18 +149,6 @@
}, this.stateDuration);
}
}
},
methods: {
tryClick(ev)
{
if (this.isDefaultState)
{
this.$emit('click', ev);
}
}
}
}
</script>
@@ -6,8 +6,7 @@
<script>
import { isArray as _isArray } from 'underscore'
import Strings from 'zero/helpers/strings.js'
import { generateId } from '../utils/numbers';
export default {
name: 'uiError',
@@ -53,14 +52,14 @@
return this.clearErrors();
}
if (!_isArray(errors))
if (!Array.isArray(errors))
{
errors = [errors];
}
errors.forEach(error =>
{
error.id = Strings.guid();
error.id = generateId();
});
if (append)
@@ -6,17 +6,17 @@
<span class="ui-select-button-icon is-image" v-if="isImage">
<img :src="source" :alt="icon" />
</span>
<content class="ui-select-button-content" v-if="label || description">
<div class="ui-select-button-content" v-if="label || description">
<strong class="ui-select-button-label" v-localize:html="{ key: label, tokens: tokens }"></strong>
<span class="ui-select-button-description" v-if="description" v-localize:html="{ key: description, tokens: tokens }"></span>
</content>
</div>
<slot></slot>
</button>
</template>
<script>
import MediaApi from 'zero/api/media.js';
//import MediaApi from 'zero/api/media.js'; // TODO vue
export default {
name: 'uiSelectButton',
@@ -53,7 +53,7 @@
},
source()
{
return this.iconAsImage ? (this.icon.indexOf('/') === 0 ? this.icon : MediaApi.getImageSource(this.icon)) : null;
return this.icon; //this.iconAsImage ? (this.icon.indexOf('/') === 0 ? this.icon : MediaApi.getImageSource(this.icon)) : null;
}
},
@@ -8,8 +8,6 @@
<script>
import { find as _find } from 'underscore';
export default {
props: {
disabled: {
@@ -6,7 +6,7 @@
<script>
import { generateId } from '../../utils';
import { generateId } from '../utils';
export default {
name: 'uiTab',
@@ -22,8 +22,6 @@
<script>
import { find as _find } from 'underscore';
export default {
props: {
label: {
@@ -1,6 +1,5 @@
import { App } from 'vue';
import { Router } from 'vue-router';
import { ZeroRuntime, ZeroInstallOptions } from './index';
@@ -12,7 +11,9 @@ export function createZeroPlugin(options?: ZeroInstallOptions)
const zero = new ZeroRuntime(app, options);
app.config.globalProperties.zero = zero;
zero.start();
zero.useZero();
zero.usePlugins();
zero.useRouter();
}
};
}
+2 -1
View File
@@ -3,4 +3,5 @@ export * from './createZeroPlugin';
export * from './zeroRuntime';
export * from './types/zero';
export * from './types/zeroPlugin';
export * from './types/zeroInstallOptions';
export * from './types/zeroInstallOptions';
export * from './types/zeroPluginOptions';
@@ -0,0 +1,18 @@
import { RouteLocationNormalized, NavigationGuardNext, nav } from 'vue-router';
export default function (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext)
{
if (from.matched.length && from.matched[0].instances)
{
let instance = from.matched[0].instances.default;
if (instance.$refs['form'] && typeof instance.$refs.form.beforeRouteLeave === 'function')
{
isGuarded = true;
instance.$refs.form.beforeRouteLeave(to, from, res =>
{
next(res !== false);
});
}
}
}
@@ -0,0 +1,47 @@
import { Zero } from '../types/zero';
import { createWebHistory, Router, RouterOptions } from 'vue-router';
import titleGuard from './titleGuard';
import formDirtyGuard from './formDirtyGuard';
export function getRouterConfig(basePath: string, zero: Zero): RouterOptions
{
let options = {
history: createWebHistory(basePath),
linkActiveClass: 'is-active',
linkExactActiveClass: 'is-active-exact',
scrollBehavior(to, from, savedPosition)
{
return savedPosition ? savedPosition : { x: 0, y: 0 };
},
routes: []
} as RouterOptions;
//options.routes.push({
// name: 'xroot',
// path: '/',
// component: () => import('../../_root.vue'),
// meta: {
// name: 'Root name',
// alias: 'rootalias',
// section: 'Root section'
// }
//} as RouteRecordRaw);
options.routes.push({ path: '/', component: () => import('../../_root.vue') });
options.routes.push({ path: '/about', component: () => import('../../_about.vue') });
// TODO add zero routes
return options;
}
export function appendRouterGuards(router: Router): Router
{
//router.beforeEach(formDirtyGuard);
//router.beforeEach(titleGuard);
return router;
}
@@ -0,0 +1,43 @@
import { localize } from '../../services/localization';
import { RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
export default function (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext)
{
let title = localize('@zero.name');
let name = to.meta.name;
if (!name && to.matched.length > 1)
{
to.matched.forEach(route =>
{
if (!name && route.meta.name)
{
name = route.meta.name;
}
});
}
if (!name || to.meta.alias === 'dashboard') //__zero.alias.sections.dashboard)
{
document.title = title;
next();
return;
}
let nameParts = Array.isArray(name) ? name : [name];
let translations = [];
nameParts.forEach(part =>
{
if (part)
{
translations.push(localize(part));
}
});
title += ': ' + translations.join(' - ');
document.title = title;
next();
}
+3 -1
View File
@@ -2,5 +2,7 @@
export interface Zero
{
version: string;
start: () => void;
useZero: () => void;
useRouter: () => void;
usePlugins: () => void;
}
@@ -1,12 +1,13 @@
import { App } from 'vue';
import { Zero } from './zero';
import { ZeroPluginOptions } from './zeroPluginOptions';
export interface ZeroPlugin
{
name: string;
install: (app: App, zero: Zero) => void;
install: (zero: ZeroPluginOptions) => void;
}
//export class Zero
@@ -0,0 +1,28 @@
import { RouteRecordRaw } from 'vue-router';
import { App } from 'vue';
export interface ZeroPluginOptions
{
vue: App;
routes: RouteRecordRaw[];
route: (route: RouteRecordRaw) => void;
}
//export class Zero
//{
// _app: App;
// _options: ZeroInstallOptions;
// get version(): string
// {
// return "0.0.1";
// }
// constructor(app: App, options?: ZeroInstallOptions)
// {
// this._app = app;
// this._options = options || {};
// console.info('[zero installed]');
// }
//}
+51 -6
View File
@@ -1,16 +1,24 @@
import { App } from 'vue';
import { Router } from 'vue-router';
import { ZeroInstallOptions } from './types/zeroInstallOptions';
import { ZeroPluginOptions } from './types/zeroPluginOptions';
import { Zero } from './types/zero';
import { createRouter, RouteRecordRaw, RouterOptions } from 'vue-router';
import registerDirectives from '../directives/register';
import registerComponents from '../components/register';
import registerFormComponents from '../forms/components/register';
import { getRouterConfig, appendRouterGuards } from './router/routerConfig';
import countryPlugin from '../modules/countries/plugin';
export class ZeroRuntime implements Zero
{
_app: App;
_options: ZeroInstallOptions;
_routerConfig: RouterOptions;
/**
* version of zero backoffice
**/
get version(): string
{
return "0.0.1";
@@ -20,13 +28,50 @@ export class ZeroRuntime implements Zero
{
this._app = app;
this._options = options || {};
console.info('[zero installed]');
this._routerConfig = getRouterConfig('/zero', this);
}
start()
/**
* register core components
**/
useZero()
{
registerDirectives(this._app);
registerComponents(this._app);
let app = this._app;
registerDirectives(app);
registerComponents(app);
registerFormComponents(app);
}
/**
* create plugin options which are passed to install() for all plugins
**/
usePlugins()
{
const pluginOptions = {
vue: this._app,
routes: this._routerConfig.routes,
route(route: RouteRecordRaw)
{
this.routes.push(route);
}
} as ZeroPluginOptions;
// install all plugins
countryPlugin.install(pluginOptions);
}
/**
* after all plugins were installed we can create the router
* with the designated routes
**/
useRouter()
{
const router = createRouter(this._routerConfig);
appendRouterGuards(router);
this._app.use(router);
}
}
+36
View File
@@ -0,0 +1,36 @@
export interface ZeroEditor
{
}
export declare type BooleanExpression<TModel, TField> = (model: TModel, value: TField) => boolean;
export interface ZeroEditorField<TModel, TField>
{
when(condition: BooleanExpression<TModel, TField>): ZeroEditorField<TModel, TField>;
required(required: boolean): ZeroEditorField<TModel, TField>;
required(condition: BooleanExpression<TModel, TField>): ZeroEditorField<TModel, TField>;
}
var field = {
when(condition)
{
return this;
},
required(required: boolean)
{
return this;
},
required(condition: BooleanExpression<any, any>)
{
return this;
}
} as ZeroEditorField<any, any>;
@@ -0,0 +1,11 @@
import uiRte from './ui-rte/ui-rte.vue';
import uiCheckList from './ui-check-list.vue';
import uiSearch from './ui-search.vue';
import uiToggle from './ui-toggle.vue';
export {
uiRte,
uiCheckList,
uiSearch,
uiToggle
};
@@ -11,8 +11,6 @@
<script>
import { map as _map } from 'underscore';
export default {
name: 'uiInputList',
@@ -21,7 +19,7 @@
type: String,
default: '@ui.add'
},
value: {
modelValue: {
type: Array,
default: () => []
},
@@ -48,28 +46,27 @@
}),
watch: {
value(value)
modelValue(value)
{
this.items = _map(value, item =>
{
return { value: item };
});
this.reload();
}
},
mounted()
{
this.items = _map(this.value, item =>
{
return { value: item };
});
this.reload();
},
methods: {
reload()
{
this.items = this.modelValue.map(item => ({ value: item }));
},
onChange()
{
this.$emit('input', _map(this.items, item => item.value));
this.$emit('update:modelValue', this.items.map(item => item.value));
},
addItem()
@@ -98,7 +95,6 @@
<style lang="scss">
.ui-input-list
{
}
.ui-input-list-item
@@ -111,7 +107,7 @@
.ui-input-list.is-disabled &
{
background: transparent;
background: transparent;
}
.ui-input
@@ -0,0 +1,174 @@
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Bold from '@tiptap/extension-bold';
import Code from '@tiptap/extension-code';
import Italic from '@tiptap/extension-italic';
//import Link from '@tiptap/extension-link';
import Strike from '@tiptap/extension-strike';
//import Underline from '@tiptap/ex';
import History from '@tiptap/extension-history';
//import Placeholder from '@tiptap/extension-placeholder';
import HorizontalRule from '@tiptap/extension-horizontal-rule';
import ListItem from '@tiptap/extension-list-item';
import BulletList from '@tiptap/extension-bullet-list';
import OrderedList from '@tiptap/extension-ordered-list';
import Heading from '@tiptap/extension-heading';
import HardBreak from '@tiptap/extension-hard-break';
export default function ()
{
return {
removeExtension(name)
{
var ext = this.extensions.find(x => x.name === name);
ext && this.extensions.splice(this.extensions.indexOf(ext), 1);
},
removeCommand(alias)
{
var cmd = this.commands.find(x => x.alias === alias);
cmd && this.commands.splice(this.commands.indexOf(cmd), 1);
},
extensions: [
Document,
Paragraph,
Text,
History,
HardBreak,
//new Link({
// target: '_blank'
//}), // TODO
Bold,
Code,
Italic,
Strike,
//Underline,
ListItem,
BulletList,
OrderedList,
HorizontalRule,
Heading.configure({ // TODO
levels: [2, 3, 4],
})
],
commands: [
//{
// alias: 'undo',
// title: '@rte.undo',
// symbol: 'fth-chevron-left',
// disabled: cmd => cmd.undoDepth() > 1,
// onClick: (ev, cmd) => cmd.undo(ev),
//},
//{
// alias: 'redo',
// title: '@rte.redo',
// symbol: 'fth-chevron-right',
// disabled: cmd => cmd.redoDepth() > 1,
// onClick: (ev, cmd) => cmd.redo(ev)
//},
{
alias: 'bold',
title: '@rte.bold',
symbol: 'fth-bold',
symbolSize: 14,
isActive: editor => editor.isActive('bold'),
onClick: editor => editor.chain().focus().toggleBold().run(),
bubble: true
},
{
alias: 'italic',
title: '@rte.italic',
symbol: 'fth-italic',
symbolSize: 14,
isActive: editor => editor.isActive('italic'),
onClick: editor => editor.chain().focus().toggleItalic().run(),
bubble: true
},
{
alias: 'underline',
title: '@rte.underline',
symbol: 'fth-underline',
symbolSize: 14,
isActive: editor => editor.isActive('underline'),
onClick: editor => editor.chain().focus().toggleUnderline().run(),
bubble: true
},
//{
// alias: 'strikethrough',
// title: '@rte.strike',
// symbol: 'fth-zap-off',
// symbolSize: 14,
// isActive: active => active.strike(),
// onClick: (ev, cmd) => cmd.strike(ev)
//},
{
alias: 'code',
title: '@rte.code',
symbol: 'fth-code',
symbolSize: 14,
isActive: editor => editor.isActive('code'),
onClick: editor => editor.chain().focus().toggleCode().run(),
bubble: true
},
{
alias: 'line',
title: '@rte.line',
symbol: 'fth-minus',
symbolSize: 15,
onClick: editor => editor.chain().focus().setHorizontalRule().run()
},
{
alias: 'list',
title: '@rte.list',
symbol: 'fth-list',
symbolSize: 14,
children: [
{
alias: 'ulist',
title: '@rte.ulist',
isActive: editor => editor.isActive('bullet_list'),
onClick: editor => editor.chain().focus().toggleBulletList().run()
},
{
alias: 'olist',
title: '@rte.olist',
isActive: editor => editor.isActive('ordered_list'),
onClick: editor => editor.chain().focus().toggleOrderedList().run()
}
]
},
{
alias: 'heading',
title: '@rte.heading',
symbol: 'fth-type',
symbolSize: 14,
children: [
{
alias: 'h2',
title: '@rte.heading2',
isActive: editor => editor.isActive('heading', { level: 2}),
onClick: editor => editor.chain().focus().toggleHeading({ level: 2 }).run()
},
{
alias: 'h3',
title: '@rte.heading3',
isActive: editor => editor.isActive('heading', { level: 3 }),
onClick: editor => editor.chain().focus().toggleHeading({ level: 3 }).run()
},
{
alias: 'h4',
title: '@rte.heading4',
isActive: editor => editor.isActive('heading', { level: 4 }),
onClick: editor => editor.chain().focus().toggleHeading({ level: 4 }).run()
}
]
}
]
};
};
@@ -0,0 +1,73 @@
//import { Node, Extension, Plugin } from '@tiptap/vue-3';
//import { chainCommands, exitCode } from 'prosemirror-commands';
//export class HardBreak extends Node
//{
// get name()
// {
// return 'br'
// }
// get schema()
// {
// return {
// inline: true,
// group: 'inline',
// selectable: false,
// parseDOM: [
// { tag: 'br' },
// ],
// toDOM: () => ['br'],
// }
// }
// keys({ type })
// {
// const command = chainCommands(exitCode, (state, dispatch) =>
// {
// dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView())
// return true
// })
// return {
// 'Enter': command
// }
// }
//}
//export class MaxSize extends Extension
//{
// get name()
// {
// return 'maxSize'
// }
// get defaultOptions()
// {
// return {
// maxSize: null
// }
// }
// get plugins()
// {
// return [
// new Plugin({
// appendTransaction: (transactions, oldState, newState) =>
// {
// const max = this.options.maxSize;
// const oldLength = oldState.doc.content.size;
// const newLength = newState.doc.content.size;
// if (max && newLength > max && newLength > oldLength)
// {
// let newTr = newState.tr;
// newTr.insertText('', max + 1, newLength);
// return newTr;
// }
// }
// })
// ]
// }
//}
@@ -100,7 +100,7 @@ export default {
return null
}
return this.$scopedSlots.default({
return this.$slots.default({
focused: this.focused,
focus: this.editor.focus,
commands: this.editor.commands,
@@ -1,34 +1,31 @@
<template>
<div class="ui-rte" :id="id" :disabled="disabled">
<editor-menu-bubble v-if="hasBubble" :editor="editor" :keep-in-bounds="true" v-slot="{ commands, isActive, menu }">
<div class="ui-rte-overlay-controls theme-dark" :class="{ 'is-active': menu.isActive }" :style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`">
<div class="ui-rte-overlay-control-outer" v-for="cmd in cmds" v-if="cmd.bubble && !cmd.isParent" :key="cmd.id">
<button type="button" :data-alias="cmd.alias" v-localize:title="cmd.title" class="ui-rte-overlay-control" :class="{ 'is-active': cmd.isActive(isActive) }" @click="cmd.onClick($event, commands)">
<!--<bubble-menu :editor="editor" :tippy-options="{ duration: 100 }" v-if="editor && hasBubble">
<div class="ui-rte-overlay-controls theme-dark is-active">
<div class="ui-rte-overlay-control-outer" v-for="cmd in editor.cmds" v-if="cmd && cmd.bubble && !cmd.isParent" :key="cmd.id">
<button type="button" :data-alias="cmd.alias" v-localize:title="cmd.title" class="ui-rte-overlay-control" :class="{ 'is-active': cmd.isActive(editor) }" @click="cmd.onClick(editor)">
<ui-icon :symbol="cmd.symbol" :size="cmd.symbolSize" />
</button>
</div>
</div>
</editor-menu-bubble>
</bubble-menu>-->
<editor-menu-bar v-if="cmds.length > 0 && editor" :editor="editor" v-slot="{ commands, isActive }">
<div class="ui-rte-controls">
<div class="ui-rte-control-outer" v-for="cmd in cmds" :key="cmd.id">
<button type="button" v-if="!cmd.isParent" :data-alias="cmd.alias" v-localize:title="cmd.title" class="ui-rte-control" :class="{ 'is-active': cmd.isActive(isActive) }" @click="cmd.onClick($event, commands)">
<!--// TODO :disabled="cmd.disabled(commands)" | this triggers a recursive loop error when used in multiple properties per editor-->
<ui-icon :symbol="cmd.symbol" :size="cmd.symbolSize" />
</button>
<ui-dropdown v-if="cmd.isParent" align="right">
<template v-slot:button>
<button :data-alias="cmd.alias" type="button" v-localize:title="cmd.title" class="ui-rte-control" :class="{ 'is-active': cmd.isActive(isActive) }">
<ui-icon :symbol="cmd.symbol" :size="cmd.symbolSize" />
</button>
</template>
<ui-dropdown-button v-for="child in cmd.children" :key="child.id" :label="child.title" @click="child.onClick($event, commands)" />
</ui-dropdown>
</div>
<div class="ui-rte-controls" v-if="editor">
<div class="ui-rte-control-outer" v-for="cmd in cmds" :key="cmd.id">
<button type="button" v-if="!cmd.isParent" :data-alias="cmd.alias" v-localize:title="cmd.title" class="ui-rte-control" :class="{ 'is-active': cmd.isActive(editor) }" @click="cmd.onClick(editor)">
<ui-icon :symbol="cmd.symbol" :size="cmd.symbolSize" />
</button>
<!--<ui-dropdown v-if="cmd.isParent" align="right">
<template v-slot:button>
<button :data-alias="cmd.alias" type="button" v-localize:title="cmd.title" class="ui-rte-control" :class="{ 'is-active': cmd.isActive(editor) }">
<ui-icon :symbol="cmd.symbol" :size="cmd.symbolSize" />
</button>
</template>
<ui-dropdown-button v-for="child in cmd.children" :key="child.id" :label="child.title" @click="child.onClick(editor)" />
</ui-dropdown>-->
</div>
</editor-menu-bar>
</div>
<editor-content class="ui-rte-input" :editor="editor" />
@@ -36,22 +33,22 @@
</template>
<script>
import Strings from 'zero/helpers/strings.js';
import { debounce as _debounce } from 'underscore';
import { Editor, EditorContent, EditorMenuBubble } from 'tiptap';
import EditorMenuBar from './rte.menubar.js';
import { MaxSize } from 'zero/config/rte.extensions.js';
import { Placeholder } from 'tiptap-extensions';
import createConfig from 'zero/config/rte.config.js';
<script lang="ts">
import { generateId } from '../../../utils/numbers';
import { debounce } from '../../../utils/timing';
import { Editor, EditorContent, BubbleMenu } from '@tiptap/vue-3';
// import { Placeholder } from '@tiptap/vue-3';
import EditorMenuBar from './rte.menubar';
//import { MaxSize } from './rte.extensions';
import createConfig from './rte.config';
export default {
name: 'uiRte',
components: { EditorContent, EditorMenuBubble, EditorMenuBar },
components: { EditorContent, BubbleMenu, EditorMenuBar },
props: {
value: {
modelValue: {
type: String,
default: ''
},
@@ -74,7 +71,7 @@
},
data: () => ({
id: 'rte-' + Strings.guid(),
id: 'rte-' + generateId(),
blocked: false,
onDebouncedChange: null,
editor: null,
@@ -83,7 +80,7 @@
}),
watch: {
value()
modelValue()
{
if (!this.blocked)
{
@@ -107,7 +104,7 @@
created()
{
this.onDebouncedChange = _debounce(this.onChange, 350);
this.onDebouncedChange = debounce(this.onChange, 350);
},
mounted()
@@ -133,48 +130,53 @@
return params;
});
if (this.placeholder)
{
this.extensions.push(new Placeholder({
emptyEditorClass: 'is-editor-empty',
emptyNodeClass: 'is-empty',
emptyNodeText: this.placeholder,
showOnlyWhenEditable: true,
showOnlyCurrent: true
}));
}
//if (this.placeholder)
//{
// this.extensions.push(new Placeholder({
// emptyEditorClass: 'is-editor-empty',
// emptyNodeClass: 'is-empty',
// emptyNodeText: this.placeholder,
// showOnlyWhenEditable: true,
// showOnlyCurrent: true
// }));
//}
if (this.maxLength > 0)
{
this.extensions.push(new MaxSize({ maxSize: this.maxLength }));
}
//if (this.maxLength > 0)
//{
// this.extensions.push(new MaxSize({ maxSize: this.maxLength }));
//}
this.editor = new Editor({
editable: !this.disabled,
extensions: this.extensions,
onUpdate: opts => this.onDebouncedChange(opts.getHTML())
onUpdate: ({ editor }) =>
{
this.onDebouncedChange(editor.getHTML());
}
});
this.editor.cmds = this.cmds;
this.init();
},
methods: {
init()
{
this.editor.setContent(this.value);
this.editor.commands.setContent(this.modelValue, false);
},
onChange(content)
{
this.blocked = true;
this.$emit('input', content);
this.$emit('update:modelValue', content);
this.$nextTick(() => this.blocked = false);
},
mapCommand(cmd)
{
return {
id: Strings.guid(),
id: generateId(),
alias: cmd.alias,
title: cmd.title,
symbol: cmd.symbol,
@@ -199,7 +201,7 @@
.ui-rte
{
background: var(--color-input);
border-radius: var(--radius);
border-radius: var(--radius);
border: var(--color-input-border);
}
@@ -359,7 +361,7 @@
.ui-rte-control-outer + .ui-rte-control-outer
{
margin-left: 5px;
margin-left: 5px;
}
.ui-rte-control:hover, .ui-rte-control.is-active
@@ -385,23 +387,24 @@
{
margin-top: 0;
}
> :last-child
{
margin-bottom: 0;
}
h1,h2,h3,h4,h5,h6
h1, h2, h3, h4, h5, h6
{
margin-bottom: .5rem;
font-weight: bold;
}
h1
h1
{
font-size: 1.4em;
}
h2
h2
{
font-size: 1.2em;
}
@@ -414,7 +417,7 @@
h4, h5, h6
{
font-size: 1em;
}
}
hr
{
@@ -423,7 +426,7 @@
border-bottom-color: var(--color-line-dashed);
}
li p
li p
{
display: block;
line-height: 1.5;
@@ -1,6 +1,6 @@
<template>
<div class="ui-searchinput">
<input ref="input" type="search" :value="value" @input="onChange" @keyup.enter="onSubmit" class="ui-input" v-localize:placeholder="placeholder" />
<input ref="input" type="search" :value="modelValue" @input="onChange" @keyup.enter="onSubmit" class="ui-input" v-localize:placeholder="placeholder" />
<slot name="button" v-bind="{ onSubmit: onSubmit }">
<button type="button" class="ui-searchinput-button" v-localize:title="'@ui.search.button'" @click="onSubmit">
<ui-icon symbol="fth-search" />
@@ -15,7 +15,7 @@
name: 'uiSearch',
props: {
value: {
modelValue: {
type: String,
default: ''
},
@@ -35,6 +35,7 @@
{
this.$emit('change', ev.target.value);
this.$emit('input', ev.target.value);
this.$emit('update:modelValue', ev.target.value);
},
onSubmit(ev)
@@ -1,6 +1,6 @@
<template>
<div class="ui-native-select" :disabled="disabled">
<select :value="value" @input="onChange" :disabled="disabled">
<select :value="modelValue" @input="onChange" :disabled="disabled">
<option v-if="emptyOption"></option>
<option v-for="option in options" :value="option.key" v-localize="option.value"></option>
</select>
@@ -13,7 +13,7 @@
name: 'uiSelect',
props: {
value: [String, Number, Object],
modelValue: [String, Number, Object],
items: [Array, Function],
entity: Object,
disabled: Boolean,
@@ -38,7 +38,7 @@
handler()
{
this.rebuild();
this.onChange({ target: { value: this.value } });
this.onChange({ target: { value: this.modelValue } });
}
},
entity: {
@@ -46,7 +46,7 @@
handler()
{
this.rebuild();
this.onChange({ target: { value: this.value } });
this.onChange({ target: { value: this.modelValue } });
}
}
},
@@ -70,7 +70,7 @@
onChange(ev)
{
this.$emit('input', ev.target.value ? ev.target.value : null);
this.$emit('update:modelValue', ev.target.value ? ev.target.value : null);
}
}
}
@@ -1,10 +1,10 @@
 <template>
<div class="ui-toggle" :class="{'is-disabled': disabled, 'is-negative': negative, 'is-active': value, 'is-content-left': contentLeft }">
<input type="checkbox" :value="value" @input="onChange" :disabled="disabled" />
<span class="ui-toggle-switch" :class="{ 'is-active': value }"><i></i></span>
<i class="fth-minus-circle ui-toggle-off-warning" v-if="offContent && !value && offWarning"></i>
<span class="ui-toggle-text" v-if="onContent && value" v-localize="onContent"></span>
<span class="ui-toggle-text" v-if="offContent && !value" v-localize="offContent"></span>
<div class="ui-toggle" :class="{'is-disabled': disabled, 'is-negative': negative, 'is-active': modelValue, 'is-content-left': contentLeft }">
<input type="checkbox" :value="modelValue" @input="onChange" :disabled="disabled" />
<span class="ui-toggle-switch" :class="{ 'is-active': modelValue }"><i></i></span>
<i class="fth-minus-circle ui-toggle-off-warning" v-if="offContent && !modelValue && offWarning"></i>
<span class="ui-toggle-text" v-if="onContent && modelValue" v-localize="onContent"></span>
<span class="ui-toggle-text" v-if="offContent && !modelValue" v-localize="offContent"></span>
</div>
</template>
@@ -14,7 +14,7 @@
name: 'uiToggle',
props: {
value: {
modelValue: {
type: Boolean,
default: false
},
@@ -48,7 +48,7 @@
onChange(ev)
{
this.$emit('input', !this.value);
this.$emit('update:modelValue', !this.modelValue);
}
}
}
+1
View File
@@ -3,6 +3,7 @@ import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './app.vue';
import { createZeroPlugin } from './core';
import './axios.config.ts';
const app = createApp(App)
.use(createPinia())
@@ -0,0 +1,30 @@
<template>
<div class="root">
countries óÒ<br />
<ui-search v-model="search" @submit="doSearch" />
<div v-for="item in items" :key="item.id">
{{item.name}}
</div>
</div>
</template>
<script lang="ts">
import api from './api';
export default {
data: () => ({
items: [],
search: null
}),
methods: {
async doSearch(val)
{
const result = await api.getByQuery({ search: val });
this.items = result.items;
}
}
}
</script>
@@ -0,0 +1,16 @@
import { get, post, put, del, ZeroRequestConfig, ZeroRequestQuery } from '../../services/request';
export default {
getEmpty: async (flavor?: string, config?: ZeroRequestConfig) => await get("countries/empty", { ...config, params: { flavor } }),
getById: async (id: string, changeVector?: string, config?: ZeroRequestConfig) => await get('countries/' + id, { ...config, params: { changeVector } }),
getByQuery: async (query: ZeroRequestQuery, config?: ZeroRequestConfig) => await get('countries', { ...config, params: { ...query } }),
create: async (model: any, config?: ZeroRequestConfig) => await post('countries', model, config),
update: async (model: any, config?: ZeroRequestConfig) => await put('countries/' + model.id, model, config),
delete: async (id: string, config?: ZeroRequestConfig) => await del('countries/' + id, config),
};
@@ -0,0 +1,15 @@
import { ZeroPlugin, ZeroPluginOptions } from '../../core';
import { defineAsyncComponent } from 'vue';
const Picker = () => import('./ui-countrypicker.vue');
const Page = () => import('./_page.vue');
export default {
name: "zero.countries",
install(app: ZeroPluginOptions)
{
app.vue.component('ui-countrypicker', defineAsyncComponent(Picker));
app.route({ path: '/countries', component: Page });
}
} as ZeroPlugin;
@@ -0,0 +1,61 @@
<template>
<div class="ui-countrypicker" :class="{'is-disabled': disabled }">
country picker
<!--<ui-pick :config="pickerConfig" :value="value" @input="onChange" :disabled="disabled" />-->
</div>
</template>
<script>
import api from './api';
export default {
name: 'uiCountrypicker',
props: {
modelValue: {
type: [String, Array],
default: null
},
limit: {
type: Number,
default: 1
},
disabled: {
type: Boolean,
default: false
},
options: {
type: Object,
default: () => {}
}
},
data: () => ({
previews: [],
pickerConfig: {}
}),
//created()
//{
// this.pickerConfig = _extend({
// scope: 'country',
// items: CountriesApi.getForPicker,
// previews: CountriesApi.getPreviews,
// limit: this.limit,
// multiple: this.limit > 1,
// preview: {
// }
// }, this.options);
//},
//methods: {
// onChange(value)
// {
// this.$emit('input', value);
// }
//}
}
</script>
+87 -60
View File
@@ -1,7 +1,34 @@
import axios from 'axios';
import { AxiosRequestConfig } from 'axios';
const getConfig = config =>
export interface ZeroRequestConfig extends AxiosRequestConfig
{
scope?: string;
}
export interface ZeroRequestQuery
{
/**
* Current page index (starts at 1)
**/
page?: number;
/**
* Items per page (defaults to 30)
**/
pageSize?: number;
/**
* Limit query to specified IDs
**/
ids?: string[];
/**
* Search query string
**/
search?: string;
}
function getConfig(config?: ZeroRequestConfig): ZeroRequestConfig
{
config = config || {};
if (config.scope)
@@ -12,33 +39,33 @@ const getConfig = config =>
return config;
};
export async function get(url, config: any = null)
export async function get(url: string, config?: ZeroRequestConfig)
{
return await send({ method: 'get', url, ...config });
}
export async function post(url, data: any = null, config: any = null)
export async function post(url: string, data: any, config?: ZeroRequestConfig)
{
return await send({ method: 'post', url, data, ...config });
}
export async function del(url, config: any = null)
export async function del(url: string, config?: ZeroRequestConfig)
{
return await send({ method: 'delete', url, ...config });
}
export async function put(url, data: any = null, config: any = null)
export async function put(url: string, data: any, config?: ZeroRequestConfig)
{
return await send({ method: 'put', url, data, ...config });
}
export async function patch(url, data: any = null, config: any = null)
export async function patch(url: string, data: any, config?: ZeroRequestConfig)
{
return await send({ method: 'patch', url, data, ...config });
}
export async function send(config)
export async function send(config: ZeroRequestConfig)
{
config = getConfig(config);
@@ -54,63 +81,63 @@ export async function send(config)
}
export function collection(base)
{
return {
getById: async (id, changeVector, config) => await get(base + 'getById', { ...config, params: { id, changeVector } }),
getByIds: async (ids, config) => await get(base + 'getByIds', { ...config, params: { ids } }),
getEmpty: async config => await get(base + 'getEmpty', { ...config }),
getByQuery: async (query, config) => await get(base + 'getByQuery', { ...config, params: { query } }),
getAll: async (config) => await get(base + 'getAll', { ...config }),
getPreviews: async (ids, config) => await get(base + 'getPreviews', { ...config, params: { ids } }),
getForPicker: async (config) => await get(base + 'getForPicker', { ...config }),
getRevisions: async (id, query, config) => await get(base + 'getRevisions', { ...config, params: { id, query } }),
save: async (model, config) => await post(base + 'save', model, { ...config }),
delete: async (id, config) => await del(base + 'delete', { ...config, params: { id } })
};
}
//export function collection(base)
//{
// return {
// getById: async (id, changeVector, config) => await get(base + 'getById', { ...config, params: { id, changeVector } }),
// getByIds: async (ids, config) => await get(base + 'getByIds', { ...config, params: { ids } }),
// getEmpty: async config => await get(base + 'getEmpty', { ...config }),
// getByQuery: async (query, config) => await get(base + 'getByQuery', { ...config, params: { query } }),
// getAll: async (config) => await get(base + 'getAll', { ...config }),
// getPreviews: async (ids, config) => await get(base + 'getPreviews', { ...config, params: { ids } }),
// getForPicker: async (config) => await get(base + 'getForPicker', { ...config }),
// getRevisions: async (id, query, config) => await get(base + 'getRevisions', { ...config, params: { id, query } }),
// save: async (model, config) => await post(base + 'save', model, { ...config }),
// delete: async (id, config) => await del(base + 'delete', { ...config, params: { id } })
// };
//}
export function download(response)
{
let filename = response.headers["zero-filename"] || 'download.bin';
//export function download(response)
//{
// let filename = response.headers["zero-filename"] || 'download.bin';
// code from: https://github.com/kennethjiang/js-file-download/blob/master/file-download.js
// // code from: https://github.com/kennethjiang/js-file-download/blob/master/file-download.js
var blob = response.data;
if (typeof window.navigator.msSaveBlob !== 'undefined')
{
// IE workaround for "HTML7007: One or more blob URLs were
// revoked by closing the blob for which they were created.
// These URLs will no longer resolve as the data backing
// the URL has been freed."
window.navigator.msSaveBlob(blob, filename);
}
else
{
var blobURL = (window.URL && window.URL.createObjectURL) ? window.URL.createObjectURL(blob) : window.webkitURL.createObjectURL(blob);
var tempLink = document.createElement('a');
tempLink.style.display = 'none';
tempLink.href = blobURL;
tempLink.setAttribute('download', filename);
// var blob = response.data;
// if (typeof window.navigator.msSaveBlob !== 'undefined')
// {
// // IE workaround for "HTML7007: One or more blob URLs were
// // revoked by closing the blob for which they were created.
// // These URLs will no longer resolve as the data backing
// // the URL has been freed."
// window.navigator.msSaveBlob(blob, filename);
// }
// else
// {
// var blobURL = (window.URL && window.URL.createObjectURL) ? window.URL.createObjectURL(blob) : window.webkitURL.createObjectURL(blob);
// var tempLink = document.createElement('a');
// tempLink.style.display = 'none';
// tempLink.href = blobURL;
// tempLink.setAttribute('download', filename);
// Safari thinks _blank anchor are pop ups. We only want to set _blank
// target if the browser does not support the HTML5 download attribute.
// This allows you to download files in desktop safari if pop up blocking
// is enabled.
if (typeof tempLink.download === 'undefined')
{
tempLink.setAttribute('target', '_blank');
}
// // Safari thinks _blank anchor are pop ups. We only want to set _blank
// // target if the browser does not support the HTML5 download attribute.
// // This allows you to download files in desktop safari if pop up blocking
// // is enabled.
// if (typeof tempLink.download === 'undefined')
// {
// tempLink.setAttribute('target', '_blank');
// }
document.body.appendChild(tempLink);
tempLink.click();
// document.body.appendChild(tempLink);
// tempLink.click();
// Fixes "webkit blob resource error 1"
setTimeout(function ()
{
document.body.removeChild(tempLink);
window.URL.revokeObjectURL(blobURL);
}, 200)
}
};
// // Fixes "webkit blob resource error 1"
// setTimeout(function ()
// {
// document.body.removeChild(tempLink);
// window.URL.revokeObjectURL(blobURL);
// }, 200)
// }
//};
+2 -1
View File
@@ -6,4 +6,5 @@ export * from './html';
export * from './numbers';
export * from './objects';
export * from './videoparser';
export * from './arrays';
export * from './arrays';
export * from './timing';
+10
View File
@@ -0,0 +1,10 @@
import { debounce as _debounce } from 'underscore';
/**
* Proxy to underscore debounce
*/
export function debounce(fn, timespan)
{
return _debounce(fn, timespan)
}
File diff suppressed because one or more lines are too long
@@ -1,10 +0,0 @@
import { get, post, put, del } from '../helpers/request.ts';
export default {
getById: async (id, config) => await get('countries/' + id, { ...config }),
getEmpty: async config => await get('countries/empty', { ...config }),
getByQuery: async (query, config) => await get('countries', { ...config, params: { query } }),
create: async (model, config) => await post('countries', model, { ...config }),
update: async (model, config) => await put('countries/' + model.id, model, { ...config }),
delete: async (id, config) => await del('countries/' + id, { ...config, params: { id } })
};
@@ -1,135 +0,0 @@
<template>
<div class="ui-alias" :class="{'is-disabled': disabled, 'is-locked': locked }">
<button type="button" class="ui-alias-lock" @click="toggleLock">
<i :class="locked ? 'fth-lock' : 'fth-unlock'"></i>
</button>
<input ref="input" :value="value" @input="onChange" type="text" class="ui-input" :maxlength="maxLength" :readonly="locked" :disabled="disabled" @blur="locked=true" />
</div>
</template>
<script>
import { map as _map } from 'underscore';
import UtilsApi from 'zero/api/utils.js';
export default {
name: 'uiAlias',
props: {
value: {
type: String,
default: null
},
name: {
type: String,
required: true,
default: null
},
disabled: {
type: Boolean,
default: false
},
maxLength: {
type: Number,
default: 200
}
},
data: () => ({
custom: false,
locked: true
}),
watch: {
name: function(val)
{
this.updateAlias(val);
}
},
mounted()
{
this.updateAlias(this.name);
},
methods: {
onChange(ev)
{
let alias = ev.target.value;
this.custom = alias !== this.value;
this.updateAlias(alias);
this.$emit('input', alias);
},
toggleLock()
{
this.locked = !this.locked;
if (!this.locked)
{
this.$nextTick(() =>
{
this.$refs.input.focus();
this.$refs.input.select();
});
}
},
updateAlias(value)
{
UtilsApi.generateAlias(value).then(alias =>
{
this.custom = alias !== this.value;
this.$emit('input', alias);
});
}
}
}
</script>
<style>
.ui-alias
{
display: flex;
align-items: center;
}
.ui-alias button
{
width: 24px;
height: 24px;
border-radius: var(--radius);
background: var(--color-box-nested);
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 13px;
margin-right: 10px;
}
.ui-alias input
{
background: transparent !important;
border: none !important;
box-shadow: none !important;
height: 24px !important;
padding: 0 !important;
outline: none !important;
min-width: 10px !important;
width: auto !important;
}
.ui-alias:not(.is-locked) input
{
font-weight: bold;
}
</style>
@@ -1,139 +0,0 @@
<template>
<div class="ui-check-list" :class="{'is-disabled': disabled, 'is-inline': inline }">
<label v-for="item in list" class="ui-native-check ui-check-list-item">
<input type="checkbox" :checked="isChecked(item)" @input="onChange(item)" :disabled="disabled" />
<span class="ui-native-check-toggle"></span>
<span v-localize="item[labelKey]"></span>
</label>
</div>
</template>
<script>
import { map as _map, filter as _filter } from 'underscore';
export default {
name: 'uiCheckList',
props: {
value: {
type: Array,
default: () => []
},
items: {
type: [Array, Function, Promise],
required: true
},
disabled: {
type: Boolean,
default: false
},
inline: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 100
},
reverse: Boolean,
labelKey: {
type: String,
default: 'value'
},
idKey: {
type: String,
default: 'key'
}
},
data: () => ({
list: []
}),
watch: {
items()
{
this.init();
}
},
mounted()
{
this.init();
},
methods: {
init()
{
if (typeof this.items === 'function')
{
this.items().then(res =>
{
this.list = res;
});
}
else
{
this.list = JSON.parse(JSON.stringify(this.items));
}
},
isChecked(item)
{
let index = this.value.indexOf(item[this.idKey]);
return (!this.reverse && index > -1) || (this.reverse && index < 0);
},
onChange(item)
{
let index = this.value.indexOf(item[this.idKey]);
let value = JSON.parse(JSON.stringify(this.value));
if (index < 0)
{
value.push(item[this.idKey]);
}
else
{
value.splice(index, 1);
}
this.$emit('input', value);
},
}
}
</script>
<style lang="scss">
.ui-check-list-item
{
display: block;
}
.ui-check-list .ui-check-list-item + .ui-check-list-item
{
margin-top: 8px;
}
.ui-alias + .ui-check-list-item
{
margin-top: 14px;
}
.ui-check-list.is-inline .ui-check-list-item
{
display: inline-block;
}
.ui-check-list.is-inline .ui-check-list-item + .ui-check-list-item
{
margin-top: 0;
margin-left: 30px;
}
.ui-check-list.is-inline .ui-check-list-item .ui-native-check-toggle
{
margin-right: 6px;
}
</style>
@@ -1,199 +0,0 @@
<template>
<div class="ui-quill" :disabled="disabled">
<div ref="editor" :id="id">
<editor-menu-bubble :editor="editor" :keep-in-bounds="keepInBounds" v-slot="{ commands, isActive, menu }">
<div class="menububble"
:class="{ 'is-active': menu.isActive }"
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`">
<button type="button" class="menububble__button"
:class="{ 'is-active': isActive.bold() }"
@click="commands.bold">
B
</button>
<button type="button" class="menububble__button"
:class="{ 'is-active': isActive.italic() }"
@click="commands.italic">
I
</button>
<button type="button" class="menububble__button"
:class="{ 'is-active': isActive.code() }"
@click="commands.code">
C
</button>
</div>
</editor-menu-bubble>
<editor-content class="editor__content" :editor="editor" />
</div>
</div>
</template>
<script>
import Strings from 'zero/helpers/strings.js';
import { Editor, EditorContent, EditorMenuBubble } from 'tiptap'
import
{
Blockquote,
BulletList,
CodeBlock,
HardBreak,
Heading,
ListItem,
OrderedList,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Strike,
Underline,
History,
} from 'tiptap-extensions'
export default {
name: 'uiQuill',
components: { EditorContent, EditorMenuBubble },
props: {
value: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
},
data: () => ({
id: 'quill-' + Strings.guid(),
blocked: false,
keepInBounds: true,
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
],
})
}),
created()
{
},
mounted()
{
this.initialize();
},
methods: {
initialize()
{
},
setValue()
{
},
onLinkCreate()
{
},
acceptHtml()
{
}
},
beforeDestroy()
{
this.editor.destroy();
},
}
</script>
<style lang="scss">
.menububble {
position: absolute;
display: flex;
z-index: 20;
background: black;
border-radius: 5px;
padding: 0.3rem;
margin-bottom: 0.5rem;
transform: translateX(-50%);
visibility: hidden;
opacity: 0;
transition: opacity 0.2s, visibility 0.2s;
&.is-active {
opacity: 1;
visibility: visible;
}
&__button {
display: inline-flex;
background: transparent;
border: 0;
color: white;
padding: 0.2rem 0.5rem;
margin-right: 0.2rem;
border-radius: 3px;
cursor: pointer;
&:last-child {
margin-right: 0;
}
&:hover {
background-color: rgba(white, 0.1);
}
&.is-active {
background-color: rgba(white, 0.2);
}
}
&__form {
display: flex;
align-items: center;
}
&__input {
font: inherit;
border: none;
background: transparent;
color: white;
}
}
</style>
@@ -1,61 +0,0 @@
<template>
<div class="ui-countrypicker" :class="{'is-disabled': disabled }">
<ui-pick :config="pickerConfig" :value="value" @input="onChange" :disabled="disabled" />
</div>
</template>
<script>
import CountriesApi from 'zero/api/countries.js'
import { extend as _extend } from 'underscore'
export default {
name: 'uiCountrypicker',
props: {
value: {
type: [String, Array],
default: null
},
limit: {
type: Number,
default: 1
},
disabled: {
type: Boolean,
default: false
},
options: {
type: Object,
default: () => {}
}
},
data: () => ({
previews: [],
pickerConfig: {}
}),
created()
{
this.pickerConfig = _extend({
scope: 'country',
items: CountriesApi.getForPicker,
previews: CountriesApi.getPreviews,
limit: this.limit,
multiple: this.limit > 1,
preview: {
}
}, this.options);
},
methods: {
onChange(value)
{
this.$emit('input', value);
}
}
}
</script>
@@ -1,103 +0,0 @@
<template>
<div class="ui-dashboard-element" :class="classList">
<header class="ui-dashboard-element-head">
<h2 class="ui-headline ui-dashboard-element-title">Element</h2>
<ui-icon-button icon="fth-more-horizontal" />
</header>
<content class="ui-dashboard-element-content">
</content>
</div>
</template>
<script>
export default {
name: 'uiDashboardElement',
props: {
value: {
type: String,
default: null
},
wide: {
type: Boolean,
default: false
},
high: {
type: Boolean,
default: false
},
theme: {
type: String,
default: 'default'
}
},
data: () => ({
id: null
}),
computed: {
classList()
{
return {
'is-high': this.high,
'is-wide': this.wide,
['theme-' + this.theme]: true
};
}
}
}
</script>
<style lang="scss">
$dashboard-element-padding: 20px;
.ui-dashboard-element
{
display: grid;
grid-template-rows: auto 1fr;
position: relative;
background: var(--color-box);
border-radius: var(--radius);
box-shadow: var(--shadow-short);
color: var(--color-text);
&.is-wide
{
grid-column-end: span 2;
}
&.is-high
{
grid-row-end: span 2;
}
}
.ui-dashboard-element-content
{
padding: $dashboard-element-padding;
}
.ui-dashboard-element-head
{
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px $dashboard-element-padding;
/*border-bottom: 1px solid var(--color-line-light);*/
.ui-icon-button
{
background: none;
margin-right: -8px;
}
}
h2.ui-dashboard-element-title
{
margin: 0;
font-size: var(--font-size);
}
</style>
@@ -11,12 +11,7 @@
</template>
<script>
import DashboardElement from '../../dashboard/element.vue';
export default {
components: { DashboardElement }
}
export default {}
</script>
<style lang="scss">
+2 -2
View File
@@ -12,8 +12,8 @@
"flatpickr": "^4.6.9",
"qs": "^6.10.2",
"sortablejs": "^1.14.0",
"tiptap": "^1.32.2",
"tiptap-extensions": "^1.35.2",
"@tiptap/vue-3": "2.0.0-beta.82",
"@tiptap/starter-kit": "2.0.0-beta.153",
"underscore": "^1.13.1",
"vue": "^3.2.24",
"vue-router": "^4.0.12",
+13 -1
View File
@@ -57,7 +57,15 @@ fs.writeFile(path.resolve(__dirname, 'app/plugins.generated.js'), pluginFileCont
let config = defineConfig({
server: {
port: process.env.PORT || 3399,
cors: true
cors: true,
proxy: {
'/zero/api': {
target: 'http://localhost:2320',
changeOrigin: true,
secure: false,
ws: false
}
}
},
plugins: [vue(), ...zeroPlugins],
build: {
@@ -67,6 +75,9 @@ let config = defineConfig({
terserOptions: {
compress: false
},
//alias: {
// 'tiptap': 'tiptap/dist/tiptap.esm.js',
//},
rollupOptions: {
output: {
format: 'es',
@@ -83,6 +94,7 @@ let config = defineConfig({
if (process.env.NODE_ENV === 'production')
{
config.base = '/zero/';
//config.alias.tiptap = 'node_modules/tiptap/dist/tiptap.esm.js';
}
export default config;