Initial commit

This commit is contained in:
Benjamin Bädorf 2020-05-20 16:21:08 +02:00
parent 76df0f29e9
commit 3062bce106
No known key found for this signature in database
GPG key ID: 4406E80E13CD656C
28 changed files with 711 additions and 97 deletions

17
package-lock.json generated
View file

@ -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",

View file

@ -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
View 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
View 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>
);
},
});

View file

@ -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>

View file

@ -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>

View 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,
},
}}
/>
);
},
});

View 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 };
}

View 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);
}
}
}

View 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>
);
},
});

View 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>
);
},
});

View 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;
}
}
}

View 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>
);
},
});

View file

@ -0,0 +1,7 @@
.sb-toolbar {
position: absolute;
bottom: 100%;
width: 100%;
height: 40px;
background-color: var(--grey-1);
}

View 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>
);
},
});

View file

View file

View file

@ -0,0 +1,11 @@
.sb-layout {
display: flex;
&_vertical {
flex-direction: column;
}
&_horizontal {
flex-direction: row;
}
}

View 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>
);
},
});

View file

@ -0,0 +1,4 @@
.sb-paragraph {
display: block;
width: 100%;
}

View 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
View 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
View 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);
}

View file

@ -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
View 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
View file

@ -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
View 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
View 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'));
},
};