Initial commit
This commit is contained in:
parent
76df0f29e9
commit
3062bce106
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -2226,6 +2226,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/composition-api/-/composition-api-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-9QDFWq7q839G1CTTaxeruPOTrrVOPSaVipJ2TxxK9QAruePNTHOGbOOFRpc8WLl4YPsu1/c29yBhMVmrXz8BZw==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^1.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@vue/eslint-config-airbnb": {
|
"@vue/eslint-config-airbnb": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/eslint-config-airbnb/-/eslint-config-airbnb-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/eslint-config-airbnb/-/eslint-config-airbnb-5.0.2.tgz",
|
||||||
|
@ -14771,8 +14779,7 @@
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "1.13.0",
|
"version": "1.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
|
||||||
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
|
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"tslint": {
|
"tslint": {
|
||||||
"version": "5.20.1",
|
"version": "5.20.1",
|
||||||
|
@ -15323,6 +15330,12 @@
|
||||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"vuex": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-ajtqwEW/QhnrBZQsZxCLHThZZaa+Db45c92Asf46ZDXu6uHXgbfVuBaJ4gzD2r4UX0oMJHstFwd2r2HM4l8umg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"w3c-hr-time": {
|
"w3c-hr-time": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vue/composition-api": "^0.5.0",
|
||||||
"core-js": "^3.6.4",
|
"core-js": "^3.6.4",
|
||||||
"vue": "^2.6.11"
|
"vue": "^2.6.11"
|
||||||
},
|
},
|
||||||
|
@ -34,6 +35,10 @@
|
||||||
"sass": "^1.26.3",
|
"sass": "^1.26.3",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"typescript": "~3.8.3",
|
"typescript": "~3.8.3",
|
||||||
"vue-template-compiler": "^2.6.11"
|
"vue-template-compiler": "^2.6.11",
|
||||||
|
"vuex": "^3.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vuex": "^3.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
src/App.scss
Normal file
7
src/App.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
36
src/App.tsx
Normal file
36
src/App.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
reactive,
|
||||||
|
watchEffect,
|
||||||
|
} from '@vue/composition-api';
|
||||||
|
import Schlechtenburg from '@components/Schlechtenburg';
|
||||||
|
|
||||||
|
import './App.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'App',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const tree = reactive({
|
||||||
|
component: 'sb-layout',
|
||||||
|
orientation: 'vertical',
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
console.log(tree);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { tree };
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="app">
|
||||||
|
<Schlechtenburg vModel={this.tree} />
|
||||||
|
|
||||||
|
<pre><code>{JSON.stringify(this.tree, null, 2)}</code></pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
29
src/App.vue
29
src/App.vue
|
@ -1,29 +0,0 @@
|
||||||
<template>
|
|
||||||
<div id="app">
|
|
||||||
<img alt="Vue logo" src="./assets/logo.png">
|
|
||||||
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from 'vue';
|
|
||||||
import HelloWorld from './components/HelloWorld.vue';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
name: 'App',
|
|
||||||
components: {
|
|
||||||
HelloWorld,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
#app {
|
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-top: 60px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,63 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<p>
|
|
||||||
For a guide and recipes on how to configure / customize this project,<br>
|
|
||||||
check out the
|
|
||||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
|
||||||
</p>
|
|
||||||
<h3>Installed CLI Plugins</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest" target="_blank" rel="noopener">unit-jest</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-e2e-nightwatch" target="_blank" rel="noopener">e2e-nightwatch</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Essential Links</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
|
||||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
|
||||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
|
||||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
|
||||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Ecosystem</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
|
||||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
|
||||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
name: 'HelloWorld',
|
|
||||||
props: {
|
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped lang="scss">
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
58
src/components/Schlechtenburg.tsx
Normal file
58
src/components/Schlechtenburg.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
computed,
|
||||||
|
reactive,
|
||||||
|
} from '@vue/composition-api';
|
||||||
|
import { model, useDynamicComponents } from '@components/TreeElement';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'schlechtenburg-main',
|
||||||
|
|
||||||
|
model,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
components: { type: Object, default: () => ({}) },
|
||||||
|
tree: { type: Object, required: true },
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const userComponents = computed(() => ({
|
||||||
|
...props.components,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { getComponent } = useDynamicComponents(userComponents);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
activeBlockId: null,
|
||||||
|
});
|
||||||
|
const activate = (id) => {
|
||||||
|
this.state.activeBlockId = id;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getComponent,
|
||||||
|
userComponents,
|
||||||
|
activate,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const Component = this.getComponent(this.tree.component);
|
||||||
|
console.log(this.tree, Component);
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
class="sb-main"
|
||||||
|
user-components={this.components}
|
||||||
|
tree={this.tree}
|
||||||
|
active-block-id={this.state.activeBlockId}
|
||||||
|
{...{
|
||||||
|
on: {
|
||||||
|
tree: (tree) => this.$emit('tree', { ...tree }),
|
||||||
|
activate: this.activate,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
42
src/components/TreeElement.ts
Normal file
42
src/components/TreeElement.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import {
|
||||||
|
reactive,
|
||||||
|
watchEffect,
|
||||||
|
PropType,
|
||||||
|
} from '@vue/composition-api';
|
||||||
|
|
||||||
|
type IComponentDefinition = { [name: string]: () => Promise<any> };
|
||||||
|
|
||||||
|
type ITreeElement = {
|
||||||
|
component: string;
|
||||||
|
id: string;
|
||||||
|
data: { [name: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
type ITreeElementProps = {
|
||||||
|
userComponents: IComponentDefinition;
|
||||||
|
id: string;
|
||||||
|
data: { [key: string]: any};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const model = {
|
||||||
|
prop: 'tree',
|
||||||
|
event: 'tree-update',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const treeElementProps = {
|
||||||
|
userComponents: {
|
||||||
|
type: (Object as unknown) as PropType<IComponentDefinition>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
component: { type: Object, required: true },
|
||||||
|
id: { type: String, required: true },
|
||||||
|
data: { type: Object, default: () => ({}) },
|
||||||
|
};
|
||||||
|
|
||||||
|
// export function useActivation
|
||||||
|
|
||||||
|
export function useDynamicComponents(components: IComponentDefinition) {
|
||||||
|
const getComponent = (name: string) => components[name];
|
||||||
|
|
||||||
|
return { getComponent };
|
||||||
|
}
|
25
src/components/internal/Block.scss
Normal file
25
src/components/internal/Block.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
.sb-block {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-items: stretch;
|
||||||
|
min-height: 50px;
|
||||||
|
|
||||||
|
> .sb-toolbar {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
outline: 1px solid var(--grey-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
> .sb-toolbar {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
outline: 1px solid var(--grey-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
src/components/internal/Block.tsx
Normal file
48
src/components/internal/Block.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { computed, defineComponent } from '@vue/composition-api';
|
||||||
|
|
||||||
|
import './Block.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'sb-block',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
active: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props, context) {
|
||||||
|
const classes = computed(() => ({
|
||||||
|
'sb-block': true,
|
||||||
|
'sb-block_active': props.active,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const activate = () => {
|
||||||
|
if (props.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.emit('activate');
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
classes,
|
||||||
|
activate,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="sb-block"
|
||||||
|
tabindex="0"
|
||||||
|
{...{
|
||||||
|
on: {
|
||||||
|
click: this.activate,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{this.$slots.toolbar}
|
||||||
|
{this.$slots.default ? this.$slots.default : <div>Your content here</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
33
src/components/internal/BlockChooser.tsx
Normal file
33
src/components/internal/BlockChooser.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { defineComponent } from '@vue/composition-api';
|
||||||
|
import { treeElementProps, useDynamicComponents } from './TreeElement';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
...treeElementProps,
|
||||||
|
orientation: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const getComponent = useDynamicComponents(props.components || {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getComponent,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div class="sb-layout">
|
||||||
|
{{ orientation }}
|
||||||
|
|
||||||
|
<component
|
||||||
|
class="sb-main"
|
||||||
|
v-for="child in children"
|
||||||
|
:key="child.id"
|
||||||
|
:is="getComponent(child.name)"
|
||||||
|
v-bind="child"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
19
src/components/internal/BlockPlaceholder.scss
Normal file
19
src/components/internal/BlockPlaceholder.scss
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
.sb-block-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 0px;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
&__add {
|
||||||
|
background-color: var(--grey-1);
|
||||||
|
height: 50px;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
src/components/internal/BlockPlaceholder.tsx
Normal file
29
src/components/internal/BlockPlaceholder.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { defineComponent } from '@vue/composition-api';
|
||||||
|
|
||||||
|
import './BlockPlaceholder.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'sb-block-placeholder',
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div class="sb-block-placeholder">
|
||||||
|
<button
|
||||||
|
class="sb-block-placeholder__add"
|
||||||
|
type="button"
|
||||||
|
{...{
|
||||||
|
on: {
|
||||||
|
click: () => this.$emit('add-block', {
|
||||||
|
component: 'sb-paragraph',
|
||||||
|
id: +(new Date()),
|
||||||
|
value: '',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{this.$slots.default ? this.$slots.default : 'Add a block'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
7
src/components/internal/Toolbar.scss
Normal file
7
src/components/internal/Toolbar.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.sb-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
background-color: var(--grey-1);
|
||||||
|
}
|
15
src/components/internal/Toolbar.tsx
Normal file
15
src/components/internal/Toolbar.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { defineComponent } from '@vue/composition-api';
|
||||||
|
|
||||||
|
import './Toolbar.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'sb-toolbar',
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div class="sb-toolbar">
|
||||||
|
{this.$slots.default}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
0
src/components/user/Heading.tsx
Normal file
0
src/components/user/Heading.tsx
Normal file
0
src/components/user/Image.tsx
Normal file
0
src/components/user/Image.tsx
Normal file
11
src/components/user/Layout.scss
Normal file
11
src/components/user/Layout.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.sb-layout {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&_vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
124
src/components/user/Layout.tsx
Normal file
124
src/components/user/Layout.tsx
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import {
|
||||||
|
reactive,
|
||||||
|
defineComponent,
|
||||||
|
watchEffect,
|
||||||
|
} from '@vue/composition-api';
|
||||||
|
import {
|
||||||
|
model,
|
||||||
|
treeElementProps,
|
||||||
|
useDynamicComponents,
|
||||||
|
} from '@components/TreeElement';
|
||||||
|
|
||||||
|
import SbBlock from '@internal/Block';
|
||||||
|
import SbToolbar from '@internal/Toolbar';
|
||||||
|
import SbBlockPlaceholder from '@internal/BlockPlaceholder';
|
||||||
|
|
||||||
|
import './Layout.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'sb-layout',
|
||||||
|
|
||||||
|
model,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
...treeElementProps,
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props, context) {
|
||||||
|
const { getComponent } = useDynamicComponents(props.userComponents);
|
||||||
|
|
||||||
|
const localData = reactive({
|
||||||
|
orientation: props.tree.data.orientation,
|
||||||
|
children: [...props.tree.data.children],
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
localData.orientation = props.tree.data.orientation;
|
||||||
|
localData.children = [...props.tree.data.children];
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = {
|
||||||
|
'sb-layout': true,
|
||||||
|
[`sb-layout_${localData.orientation}`]: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleOrientation = () => {
|
||||||
|
context.emit('data', {
|
||||||
|
id: props.blockId,
|
||||||
|
|
||||||
|
...localData,
|
||||||
|
orientation: localData.orientation === 'vertical' ? 'horizontal' : 'vertical',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChildUpdate = (child, updated) => {
|
||||||
|
const index = localData.children.indexOf(child);
|
||||||
|
context.emit('data', {
|
||||||
|
...localData,
|
||||||
|
children: [
|
||||||
|
...localData.children.slice(0, index),
|
||||||
|
updated,
|
||||||
|
...localData.children.slice(index + 1),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addBlock = (block) => {
|
||||||
|
context.emit('tree', {
|
||||||
|
...localData,
|
||||||
|
children: [
|
||||||
|
...localData.children,
|
||||||
|
block,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
classes,
|
||||||
|
onChildUpdate,
|
||||||
|
toggleOrientation,
|
||||||
|
localData,
|
||||||
|
getComponent,
|
||||||
|
addBlock,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SbBlock class={this.classes}>
|
||||||
|
<SbToolbar slot="toolbar">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
{...{
|
||||||
|
on: {
|
||||||
|
click: this.toggleOrientation,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>{this.localTree.orientation}</button>
|
||||||
|
</SbToolbar>
|
||||||
|
|
||||||
|
{...this.localTree.children.map((child) => {
|
||||||
|
const Component = this.getComponent(child.component);
|
||||||
|
return <Component
|
||||||
|
class="sb-main"
|
||||||
|
key={child.id}
|
||||||
|
components={this.components}
|
||||||
|
tree={child}
|
||||||
|
{...{
|
||||||
|
on: {
|
||||||
|
tree: (updated) => this.onChildUpdate(child, updated),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
})}
|
||||||
|
<SbBlockPlaceholder
|
||||||
|
{...{
|
||||||
|
on: {
|
||||||
|
'add-block': this.addBlock,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SbBlock>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
4
src/components/user/Paragraph.scss
Normal file
4
src/components/user/Paragraph.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.sb-paragraph {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
90
src/components/user/Paragraph.tsx
Normal file
90
src/components/user/Paragraph.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
onMounted,
|
||||||
|
} from '@vue/composition-api';
|
||||||
|
import {
|
||||||
|
model,
|
||||||
|
treeElementProps,
|
||||||
|
useTree,
|
||||||
|
} from '@components/TreeElement';
|
||||||
|
|
||||||
|
import SbBlock from '@internal/Block';
|
||||||
|
import SbToolbar from '@internal/Toolbar';
|
||||||
|
|
||||||
|
import './Paragraph.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'sb-paragraph',
|
||||||
|
|
||||||
|
model,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
...treeElementProps,
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props, context) {
|
||||||
|
const { localTree } = useTree(props);
|
||||||
|
|
||||||
|
const onTextUpdate = ($event: InputEvent) => {
|
||||||
|
localTree.value = $event.target.innerHTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
const focused = ref(false);
|
||||||
|
|
||||||
|
const classes = reactive({
|
||||||
|
'sb-paragraph': true,
|
||||||
|
'sb-paragraph_focused': focused,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFocus = () => {
|
||||||
|
console.log('focus');
|
||||||
|
focused.value = true;
|
||||||
|
};
|
||||||
|
const onBlur = () => {
|
||||||
|
console.log('blur');
|
||||||
|
focused.value = false;
|
||||||
|
context.emit('tree', {
|
||||||
|
value: localTree.value.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputEl = ref(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log(inputEl);
|
||||||
|
inputEl.value.innerHTML = localTree.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
classes,
|
||||||
|
localTree,
|
||||||
|
onTextUpdate,
|
||||||
|
focused,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
inputEl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SbBlock>
|
||||||
|
<SbToolbar>Paragraph editing</SbToolbar>
|
||||||
|
<p
|
||||||
|
class={this.classes}
|
||||||
|
ref="inputEl"
|
||||||
|
contenteditable
|
||||||
|
{...{
|
||||||
|
on: {
|
||||||
|
input: this.onTextUpdate,
|
||||||
|
focus: this.onFocus,
|
||||||
|
blur: this.onBlur,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
></p>
|
||||||
|
</SbBlock>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
15
src/lib.ts
Normal file
15
src/lib.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import storeModule from './store';
|
||||||
|
/* eslint no-param-reassign: 0 */
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(Vue, { store }: { store: Vuex }) {
|
||||||
|
|
||||||
|
store.registerModule('sb', storeModule);
|
||||||
|
|
||||||
|
Vue.component('sb-layout', () => import('@user/Layout'));
|
||||||
|
Vue.component('sb-image', () => import('@user/Image'));
|
||||||
|
Vue.component('sb-paragraph', () => import('@user/Paragraph'));
|
||||||
|
Vue.component('sb-heading', () => import('@user/Heading'));
|
||||||
|
},
|
||||||
|
};
|
18
src/main.scss
Normal file
18
src/main.scss
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
--bg: white;
|
||||||
|
--grey-1-t: rgba(0, 0, 0, 0.05);
|
||||||
|
--grey-1: rgb(242, 242, 242);
|
||||||
|
--grey-2-t: rgba(0, 0, 0, 0.1);
|
||||||
|
--grey-2: rgb(230, 230, 230);
|
||||||
|
--grey-3-t: rgba(0, 0, 0, 0.2);
|
||||||
|
--grey-3: rgb(205, 205, 205);
|
||||||
|
--grey-4-t: rgba(0, 0, 0, 0.4);
|
||||||
|
--grey-4: rgb(155, 155, 155);
|
||||||
|
--grey-5-t: rgba(0, 0, 0, 0.7);
|
||||||
|
--grey-5: rgb(75, 75, 75);
|
||||||
|
--black: rgba(0, 0, 0, 0.9);
|
||||||
|
}
|
11
src/main.ts
11
src/main.ts
|
@ -1,8 +1,17 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import App from './App.vue';
|
import Vuex from 'vuex';
|
||||||
|
import VueCompositionApi from '@vue/composition-api';
|
||||||
|
import VueSchlechtenburg from './lib';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
import './main.scss';
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
|
const store = new Vuex.Store({});
|
||||||
|
Vue.use(VueCompositionApi);
|
||||||
|
Vue.use(VueSchlechtenburg);
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
render: (h) => h(App),
|
render: (h) => h(App),
|
||||||
}).$mount('#app');
|
}).$mount('#app');
|
||||||
|
|
44
src/shims-app.d.ts
vendored
Normal file
44
src/shims-app.d.ts
vendored
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
declare module '*.bmp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.gif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.png' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.webp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.module.sass' {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
7
src/shims-tsx.d.ts
vendored
7
src/shims-tsx.d.ts
vendored
|
@ -1,11 +1,16 @@
|
||||||
|
// file: shim-tsx.d.ts
|
||||||
import Vue, { VNode } from 'vue';
|
import Vue, { VNode } from 'vue';
|
||||||
|
import { ComponentRenderProxy } from '@vue/composition-api';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
// tslint:disable no-empty-interface
|
// tslint:disable no-empty-interface
|
||||||
interface Element extends VNode {}
|
interface Element extends VNode {}
|
||||||
// tslint:disable no-empty-interface
|
// tslint:disable no-empty-interface
|
||||||
interface ElementClass extends Vue {}
|
interface ElementClass extends ComponentRenderProxy {}
|
||||||
|
interface ElementAttributesProperty {
|
||||||
|
$props: any; // specify the property name to use
|
||||||
|
}
|
||||||
interface IntrinsicElements {
|
interface IntrinsicElements {
|
||||||
[elem: string]: any;
|
[elem: string]: any;
|
||||||
}
|
}
|
||||||
|
|
35
src/store.ts
Normal file
35
src/store.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { GetterTree, MutationTree, ActionTree } from 'vuex';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
activeBlockId: number|null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const state: State = {
|
||||||
|
activeBlockId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getters = {
|
||||||
|
activeBlockId: (s) => s.activeBlockId,
|
||||||
|
} as GetterTree<State, any>;
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
setActiveBlock({ commit }, id) {
|
||||||
|
commit('setActiveBlock', id);
|
||||||
|
},
|
||||||
|
} as ActionTree<State, any>;
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
setActiveBlock(s, id) {
|
||||||
|
s.activeBlockId = id;
|
||||||
|
},
|
||||||
|
} as MutationTree<State>;
|
||||||
|
|
||||||
|
const SchlechtenburgModule = {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
mutations,
|
||||||
|
actions,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SchlechtenburgModule;
|
14
vue.config.js
Normal file
14
vue.config.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const ROOT_DIR = path.resolve(__dirname);
|
||||||
|
const SRC_DIR = path.resolve(ROOT_DIR, 'src');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
chainWebpack(config) {
|
||||||
|
config.resolve.alias
|
||||||
|
.set('@', SRC_DIR)
|
||||||
|
.set('@components', path.join(SRC_DIR, 'components'))
|
||||||
|
.set('@internal', path.join(SRC_DIR, 'components/internal'))
|
||||||
|
.set('@user', path.join(SRC_DIR, 'components/user'));
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in a new issue