diff --git a/package-lock.json b/package-lock.json
index 5a75abf..d4e4884 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@vue/eslint-config-airbnb/-/eslint-config-airbnb-5.0.2.tgz",
@@ -14771,8 +14779,7 @@
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
- "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
- "dev": true
+ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
},
"tslint": {
"version": "5.20.1",
@@ -15323,6 +15330,12 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
diff --git a/package.json b/package.json
index d29a830..61bd76a 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
+ "@vue/composition-api": "^0.5.0",
"core-js": "^3.6.4",
"vue": "^2.6.11"
},
@@ -34,6 +35,10 @@
"sass": "^1.26.3",
"sass-loader": "^8.0.2",
"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"
}
}
diff --git a/src/App.scss b/src/App.scss
new file mode 100644
index 0000000..4e300a6
--- /dev/null
+++ b/src/App.scss
@@ -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;
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..d67321b
--- /dev/null
+++ b/src/App.tsx
@@ -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 (
+
+
+
+
{JSON.stringify(this.tree, null, 2)}
+
+ );
+ },
+});
diff --git a/src/App.vue b/src/App.vue
deleted file mode 100644
index 478dd5f..0000000
--- a/src/App.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue
deleted file mode 100644
index 81871ae..0000000
--- a/src/components/HelloWorld.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
{{ msg }}
-
- For a guide and recipes on how to configure / customize this project,
- check out the
- vue-cli documentation.
-
-
Installed CLI Plugins
-
-
Essential Links
-
-
Ecosystem
-
-
-
-
-
-
-
-
diff --git a/src/components/Schlechtenburg.tsx b/src/components/Schlechtenburg.tsx
new file mode 100644
index 0000000..fd2ff16
--- /dev/null
+++ b/src/components/Schlechtenburg.tsx
@@ -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 (
+ this.$emit('tree', { ...tree }),
+ activate: this.activate,
+ },
+ }}
+ />
+ );
+ },
+});
diff --git a/src/components/TreeElement.ts b/src/components/TreeElement.ts
new file mode 100644
index 0000000..564c793
--- /dev/null
+++ b/src/components/TreeElement.ts
@@ -0,0 +1,42 @@
+import {
+ reactive,
+ watchEffect,
+ PropType,
+} from '@vue/composition-api';
+
+type IComponentDefinition = { [name: string]: () => Promise };
+
+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,
+ 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 };
+}
diff --git a/src/components/internal/Block.scss b/src/components/internal/Block.scss
new file mode 100644
index 0000000..81d4972
--- /dev/null
+++ b/src/components/internal/Block.scss
@@ -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);
+ }
+ }
+}
diff --git a/src/components/internal/Block.tsx b/src/components/internal/Block.tsx
new file mode 100644
index 0000000..6b78acb
--- /dev/null
+++ b/src/components/internal/Block.tsx
@@ -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 (
+
+ {this.$slots.toolbar}
+ {this.$slots.default ? this.$slots.default :
Your content here
}
+
+ );
+ },
+});
diff --git a/src/components/internal/BlockChooser.tsx b/src/components/internal/BlockChooser.tsx
new file mode 100644
index 0000000..1314301
--- /dev/null
+++ b/src/components/internal/BlockChooser.tsx
@@ -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 (
+
+ {{ orientation }}
+
+
+
+ );
+ },
+});
diff --git a/src/components/internal/BlockPlaceholder.scss b/src/components/internal/BlockPlaceholder.scss
new file mode 100644
index 0000000..b9a05a0
--- /dev/null
+++ b/src/components/internal/BlockPlaceholder.scss
@@ -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;
+ }
+ }
+}
diff --git a/src/components/internal/BlockPlaceholder.tsx b/src/components/internal/BlockPlaceholder.tsx
new file mode 100644
index 0000000..f78e078
--- /dev/null
+++ b/src/components/internal/BlockPlaceholder.tsx
@@ -0,0 +1,29 @@
+import { defineComponent } from '@vue/composition-api';
+
+import './BlockPlaceholder.scss';
+
+export default defineComponent({
+ name: 'sb-block-placeholder',
+
+ render() {
+ return (
+
+
+
+ );
+ },
+});
diff --git a/src/components/internal/Toolbar.scss b/src/components/internal/Toolbar.scss
new file mode 100644
index 0000000..91e998e
--- /dev/null
+++ b/src/components/internal/Toolbar.scss
@@ -0,0 +1,7 @@
+.sb-toolbar {
+ position: absolute;
+ bottom: 100%;
+ width: 100%;
+ height: 40px;
+ background-color: var(--grey-1);
+}
diff --git a/src/components/internal/Toolbar.tsx b/src/components/internal/Toolbar.tsx
new file mode 100644
index 0000000..ed90c0c
--- /dev/null
+++ b/src/components/internal/Toolbar.tsx
@@ -0,0 +1,15 @@
+import { defineComponent } from '@vue/composition-api';
+
+import './Toolbar.scss';
+
+export default defineComponent({
+ name: 'sb-toolbar',
+
+ render() {
+ return (
+
+ {this.$slots.default}
+
+ );
+ },
+});
diff --git a/src/components/user/Heading.tsx b/src/components/user/Heading.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/user/Image.tsx b/src/components/user/Image.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/user/Layout.scss b/src/components/user/Layout.scss
new file mode 100644
index 0000000..c51aa9f
--- /dev/null
+++ b/src/components/user/Layout.scss
@@ -0,0 +1,11 @@
+.sb-layout {
+ display: flex;
+
+ &_vertical {
+ flex-direction: column;
+ }
+
+ &_horizontal {
+ flex-direction: row;
+ }
+}
diff --git a/src/components/user/Layout.tsx b/src/components/user/Layout.tsx
new file mode 100644
index 0000000..f06f558
--- /dev/null
+++ b/src/components/user/Layout.tsx
@@ -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 (
+
+
+
+
+
+ {...this.localTree.children.map((child) => {
+ const Component = this.getComponent(child.component);
+ return this.onChildUpdate(child, updated),
+ },
+ }}
+ />;
+ })}
+
+
+ );
+ },
+});
diff --git a/src/components/user/Paragraph.scss b/src/components/user/Paragraph.scss
new file mode 100644
index 0000000..60c8730
--- /dev/null
+++ b/src/components/user/Paragraph.scss
@@ -0,0 +1,4 @@
+.sb-paragraph {
+ display: block;
+ width: 100%;
+}
diff --git a/src/components/user/Paragraph.tsx b/src/components/user/Paragraph.tsx
new file mode 100644
index 0000000..8879255
--- /dev/null
+++ b/src/components/user/Paragraph.tsx
@@ -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 (
+
+ Paragraph editing
+
+
+ );
+ },
+});
diff --git a/src/lib.ts b/src/lib.ts
new file mode 100644
index 0000000..6011d7d
--- /dev/null
+++ b/src/lib.ts
@@ -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'));
+ },
+};
diff --git a/src/main.scss b/src/main.scss
new file mode 100644
index 0000000..e327f4e
--- /dev/null
+++ b/src/main.scss
@@ -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);
+}
diff --git a/src/main.ts b/src/main.ts
index e5c1db2..6e5c820 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,8 +1,17 @@
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;
+const store = new Vuex.Store({});
+Vue.use(VueCompositionApi);
+Vue.use(VueSchlechtenburg);
+
new Vue({
render: (h) => h(App),
}).$mount('#app');
diff --git a/src/shims-app.d.ts b/src/shims-app.d.ts
new file mode 100644
index 0000000..fca12c1
--- /dev/null
+++ b/src/shims-app.d.ts
@@ -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;
+}
diff --git a/src/shims-tsx.d.ts b/src/shims-tsx.d.ts
index 3b88b58..cefe19f 100644
--- a/src/shims-tsx.d.ts
+++ b/src/shims-tsx.d.ts
@@ -1,11 +1,16 @@
+// file: shim-tsx.d.ts
import Vue, { VNode } from 'vue';
+import { ComponentRenderProxy } from '@vue/composition-api';
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// 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 {
[elem: string]: any;
}
diff --git a/src/store.ts b/src/store.ts
new file mode 100644
index 0000000..554180d
--- /dev/null
+++ b/src/store.ts
@@ -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;
+
+const actions = {
+ setActiveBlock({ commit }, id) {
+ commit('setActiveBlock', id);
+ },
+} as ActionTree;
+
+const mutations = {
+ setActiveBlock(s, id) {
+ s.activeBlockId = id;
+ },
+} as MutationTree;
+
+const SchlechtenburgModule = {
+ namespaced: true,
+ state,
+ getters,
+ mutations,
+ actions,
+};
+
+export default SchlechtenburgModule;
diff --git a/vue.config.js b/vue.config.js
new file mode 100644
index 0000000..34c18ca
--- /dev/null
+++ b/vue.config.js
@@ -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'));
+ },
+};