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