├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── LICENSE ├── Logo ├── Vuse_Icon_Round.png ├── Vuse_Icon_Round_Gradient.png ├── Vuse_Icon_Square.png ├── Vuse_Icon_Square_Gradient.png ├── Vuse_Logo Only.png ├── Vuse_Logo Only_Gradient.png ├── Vuse_Logo_Horizontal.png ├── Vuse_Logo_Horizontal_Gradient.png ├── Vuse_Logo_Vertical.png └── Vuse_Logo_Vertical_Gradient.png ├── README.md ├── demo ├── App.vue ├── Uploader.vue ├── app.js ├── img │ ├── airbnb.svg │ ├── atlassian.svg │ ├── avatar.png │ ├── baianat.png │ ├── covers │ │ ├── hero1.png │ │ ├── hero2.png │ │ ├── newsletter.png │ │ ├── section1.png │ │ ├── section2.png │ │ ├── social1.png │ │ ├── social2.png │ │ ├── social3.png │ │ └── social4.png │ ├── crown.svg │ ├── facebook.svg │ └── google.svg ├── index.html ├── render.html ├── sections │ ├── forms │ │ └── newsletter.vue │ ├── hero │ │ ├── hero1.vue │ │ └── hero2.vue │ ├── section │ │ ├── section1.vue │ │ └── section2.vue │ └── social │ │ ├── social1.vue │ │ ├── social2.vue │ │ ├── social3.vue │ │ └── social4.vue ├── style │ ├── _demo.styl │ ├── colors.styl │ ├── header.styl │ ├── helper.styl │ ├── main.styl │ ├── scrolling.styl │ ├── section.styl │ ├── social.styl │ ├── transition.styl │ ├── user.styl │ └── variables.styl └── webpack.config.js ├── docs ├── .vuepress │ ├── config.js │ ├── enhanceApp.js │ └── public │ │ ├── img │ │ ├── airbnb.svg │ │ ├── atlassian.svg │ │ ├── avatar.png │ │ ├── baianat.png │ │ ├── covers │ │ │ ├── hero1.png │ │ │ ├── hero2.png │ │ │ ├── newsletter.png │ │ │ ├── section1.png │ │ │ ├── section2.png │ │ │ ├── social1.png │ │ │ ├── social2.png │ │ │ ├── social3.png │ │ │ └── social4.png │ │ ├── crown.svg │ │ ├── facebook.svg │ │ ├── google.svg │ │ └── logo.png │ │ └── style.css ├── API.md ├── README.md ├── example.md ├── exporting.md ├── getting-started.md ├── section.md └── styler.md ├── package.json ├── scripts ├── build.js ├── config.js └── deploy.sh ├── src ├── components │ ├── VuseBuilder.vue │ ├── VuseIcon.js │ ├── VuseRenderer.vue │ └── VuseStyler.vue ├── index.esm.js ├── index.js ├── mixin.js ├── plugins │ ├── pwa.js │ └── scrolling.js ├── section.js ├── seeder.js ├── styler.js ├── stylus │ ├── _app.styl │ ├── colors.styl │ └── variables.styl ├── types.js ├── util.js └── vuse.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "modules": false }] 4 | ], 5 | "env": { 6 | "test": { 7 | "presets": [ 8 | ["@babel/preset-env", { "targets": { "node": "current" }}] 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard", 4 | "plugin:vue/recommended" 5 | ], 6 | 7 | "rules": { 8 | "semi": "off", 9 | "no-new": "off", 10 | "vue/valid-template-root": "off" 11 | } 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Dependency directories 9 | node_modules/ 10 | jspm_packages/ 11 | 12 | # Optional npm cache directory 13 | .npm 14 | 15 | # Optional eslint cache 16 | .eslintcache 17 | 18 | # Yarn Integrity file 19 | .yarn-integrity 20 | 21 | # dotenv environment variables file 22 | .env 23 | 24 | # visual studio code 25 | .vscode 26 | 27 | # macOS custom Icon 28 | Icon/ 29 | 30 | # vuepress output 31 | docs/.vuepress/dist 32 | 33 | # dist folders 34 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /docs 2 | /dev 3 | /src 4 | /static 5 | /scripts 6 | .* 7 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Baianat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Logo/Vuse_Icon_Round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/Logo/Vuse_Icon_Round.png -------------------------------------------------------------------------------- /Logo/Vuse_Icon_Round_Gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/Logo/Vuse_Icon_Round_Gradient.png -------------------------------------------------------------------------------- /Logo/Vuse_Icon_Square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/Logo/Vuse_Icon_Square.png -------------------------------------------------------------------------------- /Logo/Vuse_Icon_Square_Gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/Logo/Vuse_Icon_Square_Gradient.png -------------------------------------------------------------------------------- /Logo/Vuse_Logo Only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/Logo/Vuse_Logo Only.png -------------------------------------------------------------------------------- /Logo/Vuse_Logo Only_Gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/Logo/Vuse_Logo Only_Gradient.png -------------------------------------------------------------------------------- /Logo/Vuse_Logo_Horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/Logo/Vuse_Logo_Horizontal.png -------------------------------------------------------------------------------- /Logo/Vuse_Logo_Horizontal_Gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/Logo/Vuse_Logo_Horizontal_Gradient.png -------------------------------------------------------------------------------- /Logo/Vuse_Logo_Vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/Logo/Vuse_Logo_Vertical.png -------------------------------------------------------------------------------- /Logo/Vuse_Logo_Vertical_Gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/Logo/Vuse_Logo_Vertical_Gradient.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vuse 2 | 3 |

4 | 5 | 6 | 7 |

8 | 9 | > Logo by [chimzycash](https://github.com/chimzycash) 10 | 11 | **WIP: Vuse active development is now halted.** 12 | 13 | Advanced page/email builder based on [Vue.js](https://vuejs.org/). 14 | 15 | [documentation](https://baianat.github.io/vuse/) 16 | 17 | ## Credits 18 | 19 | - Logo by [chimzycash](https://github.com/chimzycash). 20 | 21 | ## License 22 | 23 | MIT 24 | -------------------------------------------------------------------------------- /demo/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /demo/Uploader.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 44 | 45 | 68 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | // Vuse scripts 2 | import Vue from 'vue'; 3 | import pwa from '../src/plugins/pwa'; 4 | import Vuse from '../src'; 5 | 6 | // demo scripts 7 | import './style/_demo.styl'; 8 | import App from './App.vue'; 9 | import Uploader from './Uploader' 10 | import hero1 from './sections/hero/hero1'; 11 | import hero2 from './sections/hero/hero2'; 12 | import section1 from './sections/section/section1'; 13 | import section2 from './sections/section/section2'; 14 | import social1 from './sections/social/social1'; 15 | import social2 from './sections/social/social2'; 16 | import social3 from './sections/social/social3'; 17 | import social4 from './sections/social/social4'; 18 | import newsletter from './sections/forms/newsletter'; 19 | 20 | // add the uploader to the list of sub-components. 21 | Vuse.mix({ 22 | components: { 23 | Uploader 24 | } 25 | }); 26 | 27 | // register components. 28 | Vuse.component(hero1); 29 | Vuse.component(hero2); 30 | Vuse.component(section1); 31 | Vuse.component(section2); 32 | Vuse.component(social1); 33 | Vuse.component(social2); 34 | Vuse.component(social3); 35 | Vuse.component(social4); 36 | Vuse.component(newsletter); 37 | 38 | // install pwa plugin. 39 | Vuse.use(pwa); 40 | 41 | // install the builder 42 | Vue.use(Vuse, { 43 | // main css file 44 | assets: { 45 | css: 'css/style.css' 46 | }, 47 | // builder default themes 48 | themes: [{ 49 | name: 'Theme 1', 50 | sections: [hero1, section1, social1, social3, newsletter] 51 | }, { 52 | name: 'Theme 2', 53 | sections: [hero2, section2, social3, social4, newsletter] 54 | }] 55 | }); 56 | 57 | new Vue({ 58 | el: '#app', 59 | render: h => h(App) 60 | }); 61 | -------------------------------------------------------------------------------- /demo/img/airbnb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/img/atlassian.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/img/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/demo/img/avatar.png -------------------------------------------------------------------------------- /demo/img/baianat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/demo/img/baianat.png -------------------------------------------------------------------------------- /demo/img/covers/hero1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/demo/img/covers/hero1.png -------------------------------------------------------------------------------- /demo/img/covers/hero2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/demo/img/covers/hero2.png -------------------------------------------------------------------------------- /demo/img/covers/newsletter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/demo/img/covers/newsletter.png -------------------------------------------------------------------------------- /demo/img/covers/section1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/demo/img/covers/section1.png -------------------------------------------------------------------------------- /demo/img/covers/section2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/demo/img/covers/section2.png -------------------------------------------------------------------------------- /demo/img/covers/social1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/demo/img/covers/social1.png -------------------------------------------------------------------------------- /demo/img/covers/social2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/demo/img/covers/social2.png -------------------------------------------------------------------------------- /demo/img/covers/social3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/demo/img/covers/social3.png -------------------------------------------------------------------------------- /demo/img/covers/social4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/demo/img/covers/social4.png -------------------------------------------------------------------------------- /demo/img/crown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /demo/img/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/img/google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vuse 7 | 8 | 9 |
10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/render.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vuse 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /demo/sections/forms/newsletter.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 55 | -------------------------------------------------------------------------------- /demo/sections/hero/hero1.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 52 | -------------------------------------------------------------------------------- /demo/sections/hero/hero2.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 52 | -------------------------------------------------------------------------------- /demo/sections/section/section1.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 69 | -------------------------------------------------------------------------------- /demo/sections/section/section2.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 78 | -------------------------------------------------------------------------------- /demo/sections/social/social1.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 66 | -------------------------------------------------------------------------------- /demo/sections/social/social2.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 47 | -------------------------------------------------------------------------------- /demo/sections/social/social3.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | -------------------------------------------------------------------------------- /demo/sections/social/social4.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 86 | -------------------------------------------------------------------------------- /demo/style/_demo.styl: -------------------------------------------------------------------------------- 1 | @import variables 2 | @import '~@baianat/base.framework/base' 3 | 4 | @import main 5 | @import header 6 | @import section 7 | @import social 8 | @import user 9 | @import helper 10 | @import scrolling 11 | 12 | .column 13 | transition: 0.2s 14 | -------------------------------------------------------------------------------- /demo/style/colors.styl: -------------------------------------------------------------------------------- 1 | /* 2 | * Color theme 3 | */ 4 | $magenta ?= #eb008b 5 | $blue ?= #0072FF 6 | $cyan ?= #00d4f0 7 | $green ?= #18d88b 8 | $yellow ?= #ffdd57 9 | $orange ?= #ffa557 10 | $red ?= #ff3d3d 11 | $purple ?= #a324ea 12 | 13 | /* 14 | * Graysacle 15 | */ 16 | $black ?= #000 17 | $dark ?= #323c47 18 | $gray ?= #c1c1c1 19 | $gray-dark ?= darken($gray, 10%) 20 | $gray-light ?= lighten($gray, 10%) 21 | $light ?= #f5f5f5 22 | $white ?= #fff 23 | -------------------------------------------------------------------------------- /demo/style/header.styl: -------------------------------------------------------------------------------- 1 | .header 2 | width: 100% 3 | padding: 50px 4 | color: $white 5 | &-title 6 | font-size: 50px 7 | line-height: 1em 8 | margin: 20px 0 9 | font-weight: lighter 10 | &-content 11 | font-size: 16px 12 | margin-bottom: 50px 13 | font-weight: lighter 14 | &-image 15 | >img 16 | width 100% 17 | &, 18 | &.is-red 19 | background-image: linear-gradient(30deg, $red, $yellow) 20 | &.is-black 21 | background-image: linear-gradient(30deg, $black, lighten($black, 30%)) 22 | &.is-green 23 | background-image: linear-gradient(30deg, $green, $yellow) 24 | &.is-blue 25 | background-image: linear-gradient(30deg, $blue, lighten($blue, 30%)) 26 | &.is-white 27 | background-image: linear-gradient(45deg, darken($white, 10%), $white) -------------------------------------------------------------------------------- /demo/style/helper.styl: -------------------------------------------------------------------------------- 1 | +prefix-classes('add-') 2 | .center-horizontal 3 | display: flex 4 | flex-direction: column 5 | justify-content: center 6 | align-items: center 7 | .center-vertical 8 | display: flex 9 | flex-direction: column 10 | justify-content: center 11 | align-items: flex-start 12 | .padding 13 | padding: 20px 14 | .text-center 15 | text-align: center 16 | .full-width 17 | width: 100% -------------------------------------------------------------------------------- /demo/style/main.styl: -------------------------------------------------------------------------------- 1 | ::selection 2 | color: inherit 3 | 4 | 5 | .button 6 | white-space: nowrap -------------------------------------------------------------------------------- /demo/style/scrolling.styl: -------------------------------------------------------------------------------- 1 | .is-active 2 | img 3 | opacity: 1 4 | transition: 0.4s 5 | p 6 | h1 7 | h2 8 | h3 9 | h6 10 | b 11 | opacity: 1 12 | transform: translate3d(0, 0, 0) 13 | transition: 0.4s 14 | 15 | .is-inactive 16 | img 17 | opacity: 0 18 | transition: 0.4s 19 | p 20 | h1 21 | h2 22 | h3 23 | h6 24 | b 25 | opacity: 0 26 | transform: translate3d(0, -200px, 0) 27 | transition: 0.4s -------------------------------------------------------------------------------- /demo/style/section.styl: -------------------------------------------------------------------------------- 1 | .section 2 | padding: 20px 30px 3 | &-title 4 | font-size: 30px 5 | color: $dark 6 | 7 | &-paragraph 8 | font-size: 16px 9 | color: lighten($dark, 20%) 10 | 11 | &.is-red 12 | background: $red 13 | &.is-black 14 | background: $black 15 | &.is-green 16 | background: $green 17 | &.is-blue 18 | background: $blue 19 | &.is-white 20 | background: $white -------------------------------------------------------------------------------- /demo/style/social.styl: -------------------------------------------------------------------------------- 1 | .social 2 | padding: 100px 0 3 | &-number 4 | font-size: 50px 5 | margin: 20px 0 50px 6 | &-item 7 | display: flex 8 | flex-direction: column 9 | justify-content: center 10 | align-items: center 11 | &-title 12 | font-size: 30px 13 | margin: 30px 0 14 | text-align: center 15 | &-content 16 | font-size: 20px 17 | margin: 0 0 40px 18 | text-align: center 19 | &-logo 20 | width: 100% 21 | margin: 20px 0 22 | @extend $flexCenter 23 | 24 | &-quote 25 | font-size: 30px 26 | color: $black 27 | quotes: "“" "”" "‘" "’" 28 | &:before 29 | content: open-quote 30 | margin-right: 20px 31 | font-size: 40px 32 | &:after 33 | content: close-quote 34 | margin-left: 20px 35 | font-size: 40px 36 | 37 | &.is-red 38 | background: $red 39 | &.is-black 40 | background: $black 41 | &.is-green 42 | background: $green 43 | &.is-blue 44 | background: $blue -------------------------------------------------------------------------------- /demo/style/transition.styl: -------------------------------------------------------------------------------- 1 | .fade-enter-active 2 | .fade-leave-active 3 | transition: all .3s ease 4 | transform: scaleX(1) 5 | 6 | .fade-enter 7 | .fade-leave-to 8 | transform: scaleX(0) -------------------------------------------------------------------------------- /demo/style/user.styl: -------------------------------------------------------------------------------- 1 | .user 2 | display: flex 3 | align-items: center 4 | margin: 10px 0 5 | &.is-alt 6 | justify-content: center 7 | flex-direction: column 8 | &-avatar 9 | border-radius: 50% 10 | width: 60px 11 | height: 60px 12 | margin-right: 20px 13 | flex-shrink: 0 14 | overflow: hidden 15 | .is-alt & 16 | margin: 0 0 20px 17 | &-data 18 | text-align: left 19 | white-space: nowrap 20 | .is-alt & 21 | text-align: center 22 | &-name 23 | font-size: 16px 24 | color: $black 25 | margin: 5px 0 26 | &-caption 27 | font-size: 16px 28 | color: $gray 29 | margin: 0 0 5px 30 | -------------------------------------------------------------------------------- /demo/style/variables.styl: -------------------------------------------------------------------------------- 1 | 2 | $flexCenter 3 | display: flex 4 | justify-content: center 5 | align-items: center -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 6 | const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin'); 7 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 8 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 9 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 10 | 11 | const env = process.env.NODE_ENV; 12 | const production = env === 'production'; 13 | 14 | // render page 15 | const page = (name) => { 16 | return new HtmlWebpackPlugin({ 17 | inject: true, 18 | template: path.join(__dirname, `./${name}.html`), 19 | filename: path.join(__dirname, `./dist/${name}.html`) 20 | }); 21 | }; 22 | 23 | const config = { 24 | mode: production ? 'production' : 'development', 25 | devtool: production ? 'source-map' : 'cheap-source-map', 26 | entry: { 27 | app: path.join(__dirname, './app.js') 28 | }, 29 | output: { 30 | path: path.join(__dirname, 'dist'), 31 | filename: 'js/[name].js' 32 | }, 33 | plugins: [ 34 | new MiniCssExtractPlugin({ 35 | filename: 'css/style.css' 36 | }), 37 | new CleanWebpackPlugin(['./dist']), 38 | new VueLoaderPlugin(), 39 | new webpack.LoaderOptionsPlugin({ options: {} }), 40 | new FriendlyErrorsWebpackPlugin(), 41 | new ProgressBarPlugin(), 42 | new CopyWebpackPlugin([{ from: path.join(__dirname, 'img'), to: './img/' }]), 43 | page('index'), 44 | page('render') 45 | ], 46 | watchOptions: { 47 | aggregateTimeout: 300, 48 | poll: 1000 49 | }, 50 | devServer: { 51 | historyApiFallback: true, 52 | hot: true, 53 | inline: true, 54 | stats: 'errors-only', 55 | host: '0.0.0.0', 56 | port: 8080 57 | }, 58 | module: { 59 | rules: [ 60 | { 61 | test: /.js$/, 62 | exclude: /node_modules/, 63 | loader: 'eslint-loader', 64 | enforce: 'pre' 65 | }, 66 | { 67 | test: /.js$/, 68 | exclude: /node_modules/, 69 | use: { 70 | loader: 'babel-loader', 71 | options: { babelrc: true } 72 | } 73 | }, 74 | { 75 | test: /\.vue$/, 76 | loader: 'eslint-loader', 77 | enforce: 'pre' 78 | }, 79 | { 80 | test: /\.vue$/, 81 | loader: 'vue-loader' 82 | }, 83 | { 84 | test: /\.css$/, 85 | loader: ['style-loader', 'css-loader'] 86 | }, 87 | { 88 | test: /\.styl(us)?$/, 89 | use: [ 90 | MiniCssExtractPlugin.loader, 91 | 'css-loader', 92 | 'stylus-loader' 93 | ] 94 | }, 95 | { 96 | test: /\.(ttf|eot|svg)(\?.*)?$/, 97 | loader: 'file-loader', 98 | options: { 99 | name: 'font/[name].[ext]' 100 | } 101 | }, 102 | { 103 | test: /.pug$/, 104 | exclude: /node_modules/, 105 | loader: 'pug-plain-loader', 106 | options: { 107 | pretty: true 108 | } 109 | } 110 | ] 111 | }, 112 | resolve: { 113 | extensions: ['.js', '.vue', '.json'] 114 | } 115 | }; 116 | 117 | module.exports = config; 118 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Vuse', 3 | description: 'Advanced page builder based on Vue.js', 4 | base: '/vuse/', 5 | themeConfig: { 6 | lastUpdated: 'Last Updated', 7 | repo: 'baianat/vuse', 8 | docsRepo: 'baianat/vuse', 9 | docsDir: 'docs', 10 | docsBranch: 'master', 11 | editLinks: true, 12 | sidebar: [ 13 | '/', 14 | '/getting-started', 15 | '/section', 16 | '/styler', 17 | '/exporting', 18 | '/API', 19 | ], 20 | nav: [ 21 | { text: 'API', link: '/API' }, 22 | { text: 'Example', link: '/example' }, 23 | ] 24 | } 25 | } -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | // Vuse scripts 2 | import Vuse from '../../src'; 3 | 4 | // demo scripts 5 | import '../../demo/style/_demo.styl'; 6 | import Uploader from '../../demo/Uploader.vue' 7 | import hero1 from '../../demo/sections/hero/hero1'; 8 | import hero2 from '../../demo/sections/hero/hero2'; 9 | import section1 from '../../demo/sections/section/section1'; 10 | import section2 from '../../demo/sections/section/section2'; 11 | import social1 from '../../demo/sections/social/social1'; 12 | import social2 from '../../demo/sections/social/social2'; 13 | import social3 from '../../demo/sections/social/social3'; 14 | import social4 from '../../demo/sections/social/social4'; 15 | import newsletter from '../../demo/sections/forms/newsletter'; 16 | 17 | // add the uploader to the list of sub-components. 18 | Vuse.mix({ 19 | components: { 20 | Uploader 21 | } 22 | }); 23 | 24 | // register components. 25 | Vuse.component(hero1); 26 | Vuse.component(hero2); 27 | Vuse.component(section1); 28 | Vuse.component(section2); 29 | Vuse.component(social1); 30 | Vuse.component(social2); 31 | Vuse.component(social3); 32 | Vuse.component(social4); 33 | Vuse.component(newsletter); 34 | 35 | export default ({ Vue }) => { 36 | // install the builder 37 | Vue.use(Vuse, { 38 | // main css file 39 | assets: { 40 | css: 'style.css' 41 | }, 42 | // builder default themes 43 | themes: [{ 44 | name: 'Theme 1', 45 | sections: [hero1, section1, social1, social3, newsletter] 46 | }, { 47 | name: 'Theme 2', 48 | sections: [hero2, section2, social3, social4, newsletter] 49 | }] 50 | }); 51 | } 52 | 53 | 54 | function normalize (component) { 55 | component.cover = component.cover.replace('static', '.'); 56 | } -------------------------------------------------------------------------------- /docs/.vuepress/public/img/airbnb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.vuepress/public/img/atlassian.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.vuepress/public/img/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/avatar.png -------------------------------------------------------------------------------- /docs/.vuepress/public/img/baianat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/baianat.png -------------------------------------------------------------------------------- /docs/.vuepress/public/img/covers/hero1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/covers/hero1.png -------------------------------------------------------------------------------- /docs/.vuepress/public/img/covers/hero2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/covers/hero2.png -------------------------------------------------------------------------------- /docs/.vuepress/public/img/covers/newsletter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/covers/newsletter.png -------------------------------------------------------------------------------- /docs/.vuepress/public/img/covers/section1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/covers/section1.png -------------------------------------------------------------------------------- /docs/.vuepress/public/img/covers/section2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/covers/section2.png -------------------------------------------------------------------------------- /docs/.vuepress/public/img/covers/social1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/covers/social1.png -------------------------------------------------------------------------------- /docs/.vuepress/public/img/covers/social2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/covers/social2.png -------------------------------------------------------------------------------- /docs/.vuepress/public/img/covers/social3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/covers/social3.png -------------------------------------------------------------------------------- /docs/.vuepress/public/img/covers/social4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/covers/social4.png -------------------------------------------------------------------------------- /docs/.vuepress/public/img/crown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /docs/.vuepress/public/img/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.vuepress/public/img/google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.vuepress/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baianat/vuse/e6678f2f6ba86e36aea5c9ba76cce04a0a3c0bf1/docs/.vuepress/public/img/logo.png -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | Your section components will have some properties injected to help you customize their behavior in building phase and production/render phase. 4 | 5 | ## $section 6 | 7 | An instance of the [section class](https://github.com/baianat/vuse/blob/master/src/js/section.js) that represents this component. 8 | 9 | ## $builder 10 | 11 | An instance of the singleton [builder class](https://github.com/baianat/vuse/blob/master/src/js/builder.js) 12 | 13 | ## $sectionData 14 | 15 | Is a computed property that mirrors `$section.data` which contains the current values (text, images, etc...) for the section. 16 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | actionText: Getting Started → 4 | actionLink: ./getting-started 5 | heroImage: /img/logo.png 6 | --- 7 | -------------------------------------------------------------------------------- /docs/example.md: -------------------------------------------------------------------------------- 1 | --- 2 | navbar: true 3 | sidebar: false 4 | editLink: false 5 | pageClass: example 6 | --- 7 | 8 | 9 | 10 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /docs/exporting.md: -------------------------------------------------------------------------------- 1 | # Exporting 2 | 3 | There are three ways to export the built page: preview, pwa or json. When clicking the save button the `b-builder` component emits a saved event with the builder instance as its payload, which exposes an `export` method. 4 | 5 | ```html 6 |
7 | 8 |
9 | 10 | 22 | ``` -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # getting-started 2 | 3 | ## What is this 4 | 5 | This builder (sections builder) reuses your Vue components as **editable sections** to produce an interactive page builder to the end user, you can use it as a prototyping tool as well as it is sort-of a block builder. 6 | 7 | The user/developer can then export the builder for usability in multiple formats, the following are the officially supported ones: 8 | 9 | - `json` A json object which can be later used to re-render a page, particularly useful if you plan to have dynamic pages or want to store them in a Database. 10 | - `preview` opens a new page without the editable logic in new tab to see the end result. 11 | - `pwa` produces a zip file which contains all the page files and images neatly packed, this is probably the format you will use for page/prototype landing page builder, The project is augmented by default with service workers to support offline viewing. 12 | 13 | The builder is just a Vue plugin, so you can integrate this into your own projects without needing to create separate bundles for it. 14 | 15 | ## Installation 16 | 17 | ### Package managers 18 | 19 | First step is to install it using `yarn` or `npm`: 20 | 21 | ```bash 22 | npm install vuse 23 | 24 | # or use yarn 25 | yarn add vuse 26 | ``` 27 | 28 | ### CDNs 29 | 30 | Or add it as a script tag in your projects. 31 | 32 | - [unpkg](https://unpkg.com/vuse) 33 | 34 | ```html 35 | 36 | 37 | ``` 38 | 39 | ### Usage 40 | 41 | ::: tip 42 | If you added it using a script tag, you can skip this section. as it will be auto-installed for you 43 | ::: 44 | 45 | ```js 46 | import Builder from 'vuse'; 47 | 48 | Vue.use(Builder); 49 | ``` 50 | 51 | You can start using the `b-builder` component to build things now. 52 | 53 | This package does not include any sections. The builder is just a system of helpers for managing customizable sections, seeding fake data, exporting views and re-rendering them. The logic for your components/sections is written by you eventually. **we will be adding a huge number of sections soon to be used with this one**. You can use the included examples after reading the API to build your own for now. -------------------------------------------------------------------------------- /docs/section.md: -------------------------------------------------------------------------------- 1 | # Section 2 | 3 | A section is the building block of the page, below is an example of a header section. 4 | ::: tip 5 | Examples use [pug](https://pugjs.org) template language to make it easier to work with templates. 6 | ::: 7 | 8 | ```pug 9 | 39 | 40 | 61 | ``` 62 | 63 | Each section has several elements that can be edited. Section data are stored in `$sectionData` object which is reactive. 64 | 65 | ## Adding the ability to edit elements in a section 66 | 67 | 1. Add `is-editable` class to it. Since editable state can be toggled off/on, it's always good to bind `is-editable` class to change when editing mode changes. e.g. `:class="{'is-editable': $builder.isEditing}"` 68 | 1. Add [`v-styler`](https://github.com/baianat/builder#v-styler) directive to the element 69 | 1. Bind the element’s innerHTML with its equivalent data e.g. `v-html="$sectionData.button.text"` 70 | 1. If you have any other data that `v-styler` changes, you have to bind it too. e.g. `:href="$sectionData.button.href"` 71 | 72 | Putting it all together 73 | 74 | ```html 75 | 81 | ``` 82 | 83 | After creating the HTML structure, you should configure the section schema to use the built-in seeder to provide you with initial/fake values when the component is instantiated in build/edit mode. Or you can set the initial values yourself instead of seeding them. 84 | 85 | ```html 86 | 130 | ``` 131 | 132 | ## Using the section 133 | 134 | Until now, we have only been creating our section component, we now need to introduce it to our builder so it can use it: 135 | 136 | ```js 137 | import Builder from 'vuse'; 138 | import section from './components/sections/section'; 139 | 140 | // Builder has Vue-like API when registering components. 141 | Builder.component(section); 142 | Vue.use(Builder); 143 | 144 | new Vue({ 145 | el: '#app' 146 | }); 147 | ``` 148 | 149 | ```html 150 |
151 | 152 |
153 | ``` 154 | 155 | You only have to register the component on the Builder plugin, which has a Vue-like API. This ensures your Vue global scope isn't polluted by the builder and keeps everything contained within the `b-builder` component. -------------------------------------------------------------------------------- /docs/styler.md: -------------------------------------------------------------------------------- 1 | # Styler 2 | 3 | This directive is automatically injected in your section components, and can be used to facilitate editing elements greatly, since it has support for multiple element types like `div`, `a`, `button` and `p` tags as well. 4 | 5 | To tell styler which variable to update, you pass it as directive expression e.g. `v-styler="$sectionData.button"` 6 | 7 | The styler directive has four types `text`, `button`, `section` or `grid`. By default, the directive can know the type implicitly, from the element tag or from the provided schema. 8 | 9 | If you want to explicitly specify the type, you can pass it as a directive modifier e.g. `v-styler.button="$sectionData.button"`. 10 | 11 | ## How to use 12 | 13 | coming soon... 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuse", 3 | "version": "0.1.1", 4 | "description": "Vue.js Page Builder", 5 | "author": "Abdelrahman Ismail ", 6 | "module": "dist/vuse.esm.js", 7 | "unpkg": "dist/vuse.min.js", 8 | "main": "dist/vuse.js", 9 | "scripts": { 10 | "dev": "webpack-dev-server --hot --inline --config ./demo/webpack.config.js", 11 | "build": "cross-env NODE_ENV=production node scripts/build.js", 12 | "build:demo": "cross-env NODE_ENV=production webpack --config ./demo/webpack.config.js", 13 | "docs:dev": "vuepress dev docs", 14 | "docs:build": "vuepress build docs", 15 | "docs:deploy": "scripts/deploy.sh", 16 | "lint": "eslint ./src --fix" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.0.0-rc.1", 20 | "@babel/plugin-proposal-class-properties": "^7.0.0-rc.1", 21 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-rc.1", 22 | "@babel/preset-env": "^7.0.0-rc.1", 23 | "babel-loader": "^8.0.0-beta", 24 | "chalk": "^2.4.1", 25 | "clean-webpack-plugin": "^0.1.19", 26 | "copy-webpack-plugin": "^4.5.1", 27 | "cross-env": "^5.0.5", 28 | "css-loader": "^1.0.0", 29 | "eslint": "^5.3.0", 30 | "eslint-config-standard": "^11.0.0", 31 | "eslint-loader": "^2.1.0", 32 | "eslint-plugin-import": "^2.7.0", 33 | "eslint-plugin-node": "^7.0.1", 34 | "eslint-plugin-promise": "^3.5.0", 35 | "eslint-plugin-standard": "^3.0.1", 36 | "eslint-plugin-vue": "^4.7.1", 37 | "file-loader": "^1.1.11", 38 | "filesize": "^3.6.1", 39 | "friendly-errors-webpack-plugin": "^1.6.1", 40 | "gzip-size": "^5.0.0", 41 | "html-webpack-plugin": "^4.0.0-alpha", 42 | "mini-css-extract-plugin": "^0.4.4", 43 | "mkdirp": "^0.5.1", 44 | "progress-bar-webpack-plugin": "^1.10.0", 45 | "pug": "^2.0.0-rc.3", 46 | "pug-plain-loader": "^1.0.0", 47 | "rollup": "^0.64.1", 48 | "rollup-plugin-buble": "^0.19.2", 49 | "rollup-plugin-commonjs": "^9.1.5", 50 | "rollup-plugin-css-only": "^0.4.0", 51 | "rollup-plugin-node-resolve": "^3.0.0", 52 | "rollup-plugin-replace": "^2.0.0", 53 | "rollup-plugin-uglify": "^4.0.0", 54 | "rollup-plugin-vue": "^4.3.2", 55 | "style-loader": "^0.22.1", 56 | "stylus": "^0.54.5", 57 | "stylus-loader": "^3.0.1", 58 | "stylus-relative-loader": "^3.4.0", 59 | "util": "^0.11.0", 60 | "vue": "^2.5.17", 61 | "vue-loader": "^15.3.0", 62 | "vue-template-compiler": "^2.5.17", 63 | "vuepress": "^0.14.8", 64 | "webpack": "^4.16.5", 65 | "webpack-cli": "^3.1.0", 66 | "webpack-dev-server": "^3.1.5" 67 | }, 68 | "dependencies": { 69 | "@baianat/base.framework": "^2.0.0-beta.0", 70 | "intersection-observer": "^0.5.0", 71 | "jszip": "^3.1.4", 72 | "lodash-es": "^4.17.4", 73 | "popper.js": "^1.14.4", 74 | "save-as": "^0.1.8", 75 | "sortablejs": "^1.6.1", 76 | "vue-server-renderer": "2.5.17" 77 | }, 78 | "license": "MIT", 79 | "files": [ 80 | "dist/*.js", 81 | "dist/*.css" 82 | ], 83 | "keywords": [ 84 | "page-builder", 85 | "vuejs", 86 | "ES6" 87 | ], 88 | "maintainers": [ 89 | { 90 | "name": "Abdelrahman Awad", 91 | "email": "logaretm1@gmail.com" 92 | }, 93 | { 94 | "name": "Abdelrahman Ismail", 95 | "email": "abdelrahman3d@gmail.com" 96 | } 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const mkdirpNode = require('mkdirp'); 3 | const { promisify } = require('util'); 4 | const { rollup } = require('rollup'); 5 | const { paths, configs, utils } = require('./config'); 6 | const mkdirp = promisify(mkdirpNode); 7 | 8 | async function buildConfig (build) { 9 | await mkdirp(paths.dist); 10 | const bundleName = build.output.file.replace(paths.dist, ''); 11 | console.log(chalk.cyan(`📦 Generating ${bundleName}...`)); 12 | 13 | const bundle = await rollup(build.input); 14 | await bundle.write(build.output); 15 | 16 | console.log(chalk.green(`👍 ${bundleName} ${utils.stats({ path: build.output.file })}`)); 17 | } 18 | 19 | async function build () { 20 | await Promise.all(Object.keys(configs).map(key => { 21 | return buildConfig(configs[key]).catch(err => { 22 | console.log(err); 23 | }); 24 | })); 25 | process.exit(0); 26 | } 27 | 28 | build(); 29 | -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const replace = require('rollup-plugin-replace'); 4 | const vue = require('rollup-plugin-vue').default; 5 | const resolve = require('rollup-plugin-node-resolve'); 6 | const css = require('rollup-plugin-css-only'); 7 | const buble = require('rollup-plugin-buble'); 8 | const commonjs = require('rollup-plugin-commonjs'); 9 | const filesize = require('filesize'); 10 | const gzipSize = require('gzip-size'); 11 | const { uglify } = require('rollup-plugin-uglify'); 12 | 13 | const version = process.env.VERSION || require('../package.json').version; 14 | 15 | const common = { 16 | banner: 17 | `/** 18 | * Vuse v${version} 19 | * (c) ${new Date().getFullYear()} Baianat 20 | * @license MIT 21 | */`, 22 | paths: { 23 | input: path.join(__dirname, '../src/index.js'), 24 | src: path.join(__dirname, '../src/'), 25 | dist: path.join(__dirname, '../dist/') 26 | }, 27 | builds: { 28 | umd: { 29 | file: 'vuse.js', 30 | format: 'umd', 31 | name: 'vuse', 32 | env: 'development' 33 | }, 34 | umdMin: { 35 | file: 'vuse.min.js', 36 | format: 'umd', 37 | name: 'vuse', 38 | env: 'production' 39 | }, 40 | esm: { 41 | input: path.join(__dirname, '../src/index.esm.js'), 42 | file: 'vuse.esm.js', 43 | format: 'es' 44 | } 45 | } 46 | }; 47 | 48 | function genConfig (options) { 49 | const config = { 50 | description: '', 51 | input: { 52 | input: options.input || common.paths.input, 53 | plugins: [ 54 | commonjs(), 55 | replace({ __VERSION__: version }), 56 | css(), 57 | vue({ css: false }), 58 | resolve(), 59 | buble() 60 | ] 61 | }, 62 | output: { 63 | banner: common.banner, 64 | name: options.name, 65 | format: options.format, 66 | file: path.join(common.paths.dist, options.file) 67 | } 68 | }; 69 | 70 | if (options.env) { 71 | config.input.plugins.unshift(replace({ 72 | 'process.env.NODE_ENV': JSON.stringify(options.env) 73 | })); 74 | } 75 | 76 | if (options.env === 'production') { 77 | config.input.plugins.push(uglify()); 78 | } 79 | 80 | return config; 81 | }; 82 | 83 | const configs = Object.keys(common.builds).reduce((prev, key) => { 84 | prev[key] = genConfig(common.builds[key]); 85 | 86 | return prev; 87 | }, {}); 88 | 89 | module.exports = { 90 | configs, 91 | uglifyOptions: common.uglifyOptions, 92 | paths: common.paths, 93 | utils: { 94 | stats ({ path }) { 95 | const code = fs.readFileSync(path); 96 | const { size } = fs.statSync(path); 97 | const gzipped = gzipSize.sync(code); 98 | 99 | return `| Size: ${filesize(size)} | Gzip: ${filesize(gzipped)}`; 100 | } 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | npm run docs:build 6 | 7 | cd docs/.vuepress/dist 8 | 9 | git init 10 | git add -A 11 | git commit -m 'deploy' 12 | 13 | git push -f git@github.com:baianat/vuse.git master:gh-pages 14 | 15 | cd - -------------------------------------------------------------------------------- /src/components/VuseBuilder.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 264 | 265 | 446 | -------------------------------------------------------------------------------- /src/components/VuseIcon.js: -------------------------------------------------------------------------------- 1 | const icons = { 2 | plus: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z', 3 | tic: 'M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z', 4 | sort: 'M14 5h8v2h-8zm0 5.5h8v2h-8zm0 5.5h8v2h-8zM2 11.5C2 15.08 4.92 18 8.5 18H9v2l3-3-3-3v2h-.5C6.02 16 4 13.98 4 11.5S6.02 7 8.5 7H12V5H8.5C4.92 5 2 7.92 2 11.5z', 5 | link: 'M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z', 6 | palettes: 'M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z', 7 | close: 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z', 8 | bold: 'M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z', 9 | italic: 'M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z', 10 | underline: 'M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z', 11 | center: 'M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z', 12 | left: 'M15 15H3v2h12v-2zm0-8H3v2h12V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z', 13 | right: 'M3 21h18v-2H3v2zm6-4h12v-2H9v2zm-6-4h18v-2H3v2zm6-4h12V7H9v2zM3 3v2h18V3H3z', 14 | trash: 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z', 15 | align: 'M3 21h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18V7H3v2zm0-6v2h18V3H3z', 16 | textStyle: 'M23 7V1h-6v2H7V1H1v6h2v10H1v6h6v-2h10v2h6v-6h-2V7h2zM3 3h2v2H3V3zm2 18H3v-2h2v2zm12-2H7v-2H5V7h2V5h10v2h2v10h-2v2zm4 2h-2v-2h2v2zM19 5V3h2v2h-2zm-5.27 9h-3.49l-.73 2H7.89l3.4-9h1.4l3.41 9h-1.63l-.74-2zm-3.04-1.26h2.61L12 8.91l-1.31 3.83z', 17 | section: 'M10 18h5v-6h-5v6zm-6 0h5V5H4v13zm12 0h5v-6h-5v6zM10 5v6h11V5H10z', 18 | arrowDown: 'M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z', 19 | arrowRight: 'M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z', 20 | arrowLeft: 'M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z', 21 | mobile: 'M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38 0 2.5-1.12 2.5-2.5v-17C18 2.12 16.88 1 15.5 1zm-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5-4H7V4h9v14z', 22 | tablet: 'M21 4H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h18c1.1 0 1.99-.9 1.99-2L23 6c0-1.1-.9-2-2-2zm-2 14H5V6h14v12z', 23 | laptop: 'M20 18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H0v2h24v-2h-4zM4 6h16v10H4V6z', 24 | monitor: 'M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7v2H8v2h8v-2h-2v-2h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H3V4h18v12z', 25 | download: 'M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z', 26 | eye: 'M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z', 27 | undo: 'M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z' 28 | }; 29 | 30 | export default { 31 | functional: true, 32 | props: { 33 | name: { 34 | type: String, 35 | required: true, 36 | validator: (val) => { 37 | if (!(val in icons) && process.env.NODE_ENV !== 'production') { 38 | console.warn(`Invalid icon name "${val}"`); 39 | return false; 40 | } 41 | 42 | return true; 43 | } 44 | } 45 | }, 46 | render (h, { props }) { 47 | const path = h('path', { 48 | attrs: { 49 | d: icons[props.name] 50 | } 51 | }); 52 | 53 | return h( 54 | 'svg', 55 | { 56 | attrs: { 57 | version: '1.1', 58 | xmlns: 'http://www.w3.org/2000/svg', 59 | class: 'vuse-icon', 60 | viewBox: '0 0 24 24' 61 | } 62 | }, 63 | [path] 64 | ); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/components/VuseRenderer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 29 | -------------------------------------------------------------------------------- /src/components/VuseStyler.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 259 | 260 | 362 | -------------------------------------------------------------------------------- /src/index.esm.js: -------------------------------------------------------------------------------- 1 | import Vuse from './vuse'; 2 | import * as types from './types'; 3 | 4 | const version = '__VERSION__'; 5 | 6 | // Auto install if Vue is defined globally. 7 | if (typeof Vue !== 'undefined') { 8 | // eslint-disable-next-line 9 | Vue.use(Builder); 10 | } 11 | 12 | export { 13 | Vuse, 14 | types, 15 | version 16 | }; 17 | 18 | export default Vuse; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Vuse from './vuse'; 2 | import * as types from './types'; 3 | 4 | // Auto install if Vue is defined globally. 5 | if (typeof Vue !== 'undefined') { 6 | // eslint-disable-next-line 7 | Vue.use(Builder); 8 | } 9 | 10 | Vuse.version = '__VERSION__'; 11 | Vuse.types = types; 12 | 13 | export default Vuse; 14 | -------------------------------------------------------------------------------- /src/mixin.js: -------------------------------------------------------------------------------- 1 | function installMixin ({ builder }) { 2 | builder.mixin = { 3 | provide: function providesBuilder () { 4 | const provides = {}; 5 | if (this.$builder) { 6 | provides.$builder = this.$builder; 7 | } 8 | 9 | if (this.$section) { 10 | provides.$section = this.$section; 11 | } 12 | 13 | return provides; 14 | }, 15 | beforeCreate () { 16 | this.$builder = builder; 17 | if (!this.$options.propsData || this.$options.propsData.id === undefined) { 18 | return; 19 | } 20 | this.$section = this.$builder.find(this.$options.propsData.id); 21 | this.$options.computed = { 22 | $sectionData: function getSectionData () { 23 | return this.$section.data; 24 | }, 25 | gridClasses: function getGridClasses () { 26 | return this.$sectionData.columns.map(column => { 27 | return Object.keys(column.grid).map(device => { 28 | if (!column.grid[device]) { 29 | return ''; 30 | } 31 | const prefix = this.$builder.columnsPrefix[device] 32 | return `${prefix}${column.grid[device]}`; 33 | }); 34 | }) 35 | } 36 | } 37 | }, 38 | updated () { 39 | Array.from(this.$el.querySelectorAll('[contentEditable]')).forEach((el) => { 40 | el.contentEditable = this.$builder.isEditing; 41 | }); 42 | } 43 | }; 44 | }; 45 | 46 | export default installMixin; 47 | -------------------------------------------------------------------------------- /src/plugins/pwa.js: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip'; 2 | import saveAs from 'save-as'; 3 | import { getImageBlob, cleanDOM } from '../../src/util'; 4 | 5 | /** 6 | * Adds a service worker that caches the static assets. 7 | */ 8 | function createSW (output, { images = [] } = {}) { 9 | output.file('sw.js', ` 10 | const staticCacheName = 'bbuilder-static-v1'; 11 | 12 | self.addEventListener('install', function(event) { 13 | event.waitUntil( 14 | caches.open(staticCacheName).then(function(cache) { 15 | return cache.addAll([ 16 | '/', 17 | '/assets/js/main.js', 18 | ${images.map(i => "'/assets/img/" + i.name + "'").join(',').trim(',')} 19 | ]); 20 | }) 21 | ); 22 | }); 23 | 24 | function serveAsset(request) { 25 | return caches.open(staticCacheName).then(function(cache) { 26 | return cache.match(request).then(function(response) { 27 | if (response) return response; 28 | 29 | return fetch(request).then(function(networkResponse) { 30 | cache.put(request, networkResponse.clone()); 31 | return networkResponse; 32 | }); 33 | }); 34 | }); 35 | } 36 | 37 | self.addEventListener('fetch', function(event) { 38 | const requestUrl = new URL(event.request.url); 39 | 40 | if (requestUrl.origin === location.origin) { 41 | if (requestUrl.pathname === '/') { 42 | event.respondWith(caches.match('/')); 43 | return; 44 | } 45 | if (requestUrl.pathname.startsWith('/assets/')) { 46 | event.respondWith(serveAsset(event.request)); 47 | return; 48 | } 49 | } 50 | 51 | event.respondWith( 52 | caches.match(event.request).then(function(response) { 53 | return response || fetch(event.request); 54 | }) 55 | ); 56 | }); 57 | `); 58 | 59 | const scripts = output.folder('assets/js'); 60 | scripts.file('main.js', ` 61 | function registerSW () { 62 | if (!navigator.serviceWorker) return; 63 | navigator.serviceWorker.register('/sw.js').then(function (reg) { 64 | console.log('SW registered!'); 65 | }); 66 | } 67 | 68 | registerSW(); 69 | `); 70 | } 71 | 72 | /** 73 | * Adds some PWA features. 74 | */ 75 | function createPWA (output, payload) { 76 | createSW(output, payload); 77 | } 78 | 79 | function download (assets) { 80 | const frag = this.outputFragment(); 81 | const images = Array.from(frag.querySelectorAll('img')); 82 | const artboard = frag.querySelector('#artboard'); 83 | const title = document.title; 84 | const zip = new JSZip(); 85 | const output = zip.folder('project'); 86 | const imgFolder = output.folder('assets/img'); 87 | const cssFolder = output.folder('assets/css'); 88 | 89 | Promise.all(images.map((image) => { 90 | const imageLoader = getImageBlob(image.src); 91 | return imageLoader.then((img) => { 92 | imgFolder.file(img.name, img.blob, { base64: true }); 93 | image.setAttribute('src', `assets/img/${img.name}`); 94 | 95 | return img; 96 | }); 97 | })).then(images => { 98 | createPWA(output, { images }); 99 | }).then(() => { 100 | return new Promise((resolve, reject) => { 101 | const assetsClient = new XMLHttpRequest(); 102 | assetsClient.open('GET', assets.css); 103 | assetsClient.onload = function () { 104 | resolve(this.response); 105 | } 106 | assetsClient.send(null); 107 | }).then((content) => { 108 | cssFolder.file('app.css', content); 109 | return content; 110 | }); 111 | }).then(() => { 112 | cleanDOM(frag); 113 | output.file('index.html', 114 | ` 115 | 116 | ${title} 117 | 118 | 119 | 120 | ${artboard.innerHTML} 121 | 122 | 123 | `); 124 | 125 | zip.generateAsync({ type: 'blob' }).then((blob) => { 126 | saveAs(blob, 'project.zip'); 127 | }); 128 | }); 129 | } 130 | 131 | export default function install ({ builder }) { 132 | builder.download = download; 133 | }; 134 | -------------------------------------------------------------------------------- /src/plugins/scrolling.js: -------------------------------------------------------------------------------- 1 | import 'intersection-observer'; 2 | 3 | const callback = (entries, observer) => { 4 | entries.forEach(entry => { 5 | if (entry.intersectionRatio > 0.5) { 6 | entry.target.classList.add('is-active'); 7 | entry.target.classList.remove('is-inactive'); 8 | return; 9 | } 10 | entry.target.classList.add('is-inactive'); 11 | entry.target.classList.remove('is-active'); 12 | }); 13 | } 14 | 15 | const observer = new IntersectionObserver(callback, { 16 | root: null, 17 | rootMargin: '0px', 18 | threshold: [0.1, 0.5, 0.9, 1] 19 | }); 20 | 21 | const scrolling = (rootEl) => { 22 | if (!rootEl) return; 23 | let sections = Array.from(rootEl.querySelectorAll('section')); 24 | sections.forEach(section => { 25 | section.classList.add('is-inactive'); 26 | observer.observe(section); 27 | }); 28 | } 29 | 30 | export default function install ({ builder }) { 31 | builder.scrolling = scrolling; 32 | }; 33 | -------------------------------------------------------------------------------- /src/section.js: -------------------------------------------------------------------------------- 1 | import getPath from 'lodash-es/get'; 2 | import toPath from 'lodash-es/toPath'; 3 | import Seeder from './seeder'; 4 | 5 | const SECTION_OPTIONS = { 6 | name: null, 7 | schema: {} 8 | }; 9 | 10 | let counter = 0; 11 | 12 | export default class Section { 13 | constructor (options) { 14 | this.id = counter++; 15 | options = Object.assign({}, SECTION_OPTIONS, options); 16 | this.name = options.name; 17 | this.schema = options.schema; 18 | this.data = options.data || Seeder.seed(options.schema); 19 | this.stylers = []; 20 | } 21 | 22 | set (name, value) { 23 | const path = toPath(name); 24 | const prop = path.pop(); 25 | 26 | path.shift(); 27 | const obj = path.length === 0 ? this.data : getPath(this.data, path); 28 | if (typeof value === 'function') { 29 | value(obj[prop]); 30 | return; 31 | } 32 | 33 | obj[prop] = value; 34 | } 35 | 36 | get (name) { 37 | const path = toPath(name); 38 | const prop = path.pop(); 39 | path.shift(); 40 | const obj = path.length === 0 ? this.data : getPath(this.data, path); 41 | 42 | return obj[prop]; 43 | } 44 | 45 | destroy () { 46 | this.stylers.forEach(styler => styler.$destroy()) 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/seeder.js: -------------------------------------------------------------------------------- 1 | import * as types from './types'; 2 | import { isObject } from './util'; 3 | 4 | const ASSETS_DIR = '.'; 5 | const data = new Map([ 6 | [types.Title, 'Awesome title'], 7 | [types.Text, 'We\'re creating the best place to go when starting a new business or company.With Baianat you can instantly search domain names, social media handles, and see your logo in beautiful logotypes.'], 8 | [types.Avatar, `${ASSETS_DIR}/img/avatar.png`], 9 | [types.Logo, `${ASSETS_DIR}/img/google.svg`], 10 | [types.Link, 'http://example.com'], 11 | [types.Image, `${ASSETS_DIR}/img/baianat.png`], 12 | [types.ClassList, () => []], 13 | [types.Button, () => ({ text: 'Click Me!', classes: [], href: 'http://example.com' })], 14 | [types.Quote, 'When you were made a leader, you weren\'t given a crown; you were given the responsibility to bring out the best in others.'], 15 | [types.Grid, () => ({mobile: '', tablet: '', desktop: '', widescreen: ''})], 16 | [Number, 100], 17 | [String, 'This is pretty neat'] 18 | ]); 19 | 20 | export default class Seeder { 21 | // Seeds values using a schema. 22 | static seed (schema) { 23 | if (isObject(schema)) { 24 | return Object.keys(schema).reduce((values, key) => { 25 | values[key] = Seeder.seed(schema[key]); 26 | return values; 27 | }, {}); 28 | } else if (Array.isArray(schema)) { 29 | return schema.map(s => { 30 | return Seeder.seed(s) 31 | }); 32 | } 33 | 34 | let value = data.get(schema); 35 | if (value === undefined) { 36 | value = schema; 37 | } 38 | return typeof value === 'function' ? value() : value; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/styler.js: -------------------------------------------------------------------------------- 1 | import Styler from './components/VuseStyler.vue'; 2 | import { getTypeFromTagName, getTypeFromSchema } from './util'; 3 | 4 | function installStyler ({ builder, Vue }) { 5 | const StylerInstance = Vue.extend(Styler).extend({ 6 | beforeCreate () { 7 | this.$builder = builder; 8 | } 9 | }); 10 | 11 | builder.styler = { 12 | inserted (el, binding, vnode) { 13 | const newNode = document.createElement('div'); 14 | const section = vnode.context.$section; 15 | const rootApp = vnode.context.$root.$el; 16 | rootApp.appendChild(newNode); 17 | el.classList.add('is-editable'); 18 | section.stylers.push(new StylerInstance({ 19 | propsData: { 20 | el, 21 | section: section, 22 | type: binding.arg || getTypeFromSchema(binding.expression, section.schema) || getTypeFromTagName(el.tagName), 23 | name: binding.expression 24 | } 25 | }).$mount(newNode)); 26 | } 27 | }; 28 | }; 29 | 30 | export default installStyler; 31 | -------------------------------------------------------------------------------- /src/stylus/_app.styl: -------------------------------------------------------------------------------- 1 | @import variables 2 | @import colors 3 | 4 | .vuse-icon 5 | display: block 6 | width: 20px 7 | height: 20px 8 | $floatHover 9 | cursor: pointer 10 | box-shadow: 0 14px 28px alpha($black, 0.125), 0 10px 10px alpha($black, 0.1) -------------------------------------------------------------------------------- /src/stylus/colors.styl: -------------------------------------------------------------------------------- 1 | /* 2 | * Color theme 3 | */ 4 | $magenta ?= #eb008b 5 | $blue ?= #0072FF 6 | $cyan ?= #00d4f0 7 | $green ?= #18d88b 8 | $yellow ?= #ffdd57 9 | $orange ?= #ffa557 10 | $red ?= #ff3d3d 11 | $purple ?= #a324ea 12 | 13 | /* 14 | * Graysacle 15 | */ 16 | $black ?= #000 17 | $dark ?= #323c47 18 | $gray ?= #c1c1c1 19 | $gray-dark ?= darken($gray, 10%) 20 | $gray-light ?= lighten($gray, 10%) 21 | $light ?= #f5f5f5 22 | $white ?= #fff 23 | -------------------------------------------------------------------------------- /src/stylus/variables.styl: -------------------------------------------------------------------------------- 1 | 2 | $flexCenter 3 | display: flex 4 | justify-content: center 5 | align-items: center -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | export class Avatar {}; 2 | 3 | export class Title {}; 4 | 5 | export class Text {}; 6 | 7 | export class Logo {}; 8 | 9 | export class Image {}; 10 | 11 | export class Quote {}; 12 | 13 | export class Link {}; 14 | 15 | export class ClassList {}; 16 | 17 | export class Button {}; 18 | 19 | export class Grid { }; 20 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import getPath from 'lodash/get'; 2 | import * as types from './types'; 3 | 4 | export function isObject (obj) { 5 | return obj && typeof obj === 'object' && obj !== null && !Array.isArray(obj); 6 | }; 7 | 8 | export function isParentTo (target, parent) { 9 | let currentNode = target; 10 | while (currentNode !== null) { 11 | if (currentNode === parent) return true; 12 | currentNode = currentNode.parentNode; 13 | } 14 | return false; 15 | } 16 | 17 | /** 18 | * 19 | * @param {String} target 20 | * @param {Object} schema 21 | */ 22 | export function getTypeFromSchema (target, schema) { 23 | const tempTarget = target.split('.'); 24 | tempTarget.shift(); 25 | const value = getPath(schema, tempTarget.join('.')); 26 | if (value === types.Grid) return 'grid'; 27 | if (value === types.Text) return 'text'; 28 | if (value === types.Title) return 'text'; 29 | if (value === types.Button) return 'button'; 30 | if (value === types.ClassList) return 'section'; 31 | if (value === String) return 'text'; 32 | if (value === Number) return 'text'; 33 | 34 | return null; 35 | } 36 | 37 | export function getImageBlob (URL) { 38 | return new Promise((resolve, reject) => { 39 | const xhr = new XMLHttpRequest(); 40 | xhr.open('GET', URL); 41 | xhr.responseType = 'blob'; 42 | 43 | xhr.onload = function () { 44 | const imageBlob = this.response; 45 | const fileType = this.response.type.split('/')[1].split('+')[0]; 46 | const randomNumber = new Date().getUTCMilliseconds(); 47 | const filename = `image-${randomNumber}.${fileType}`; 48 | resolve({ blob: imageBlob, name: filename }); 49 | } 50 | xhr.send(null); 51 | }); 52 | } 53 | 54 | export function getTypeFromTagName (tagName) { 55 | tagName = tagName.toUpperCase(); 56 | switch (tagName) { 57 | case 'H1': 58 | return 'text'; 59 | case 'H2': 60 | return 'text'; 61 | case 'H3': 62 | return 'text'; 63 | case 'H4': 64 | return 'text'; 65 | case 'H5': 66 | return 'text'; 67 | case 'H6': 68 | return 'text'; 69 | case 'P': 70 | return 'text'; 71 | case 'B': 72 | return 'text'; 73 | case 'SPAN': 74 | return 'text'; 75 | case 'BUTTON': 76 | return 'button'; 77 | case 'A': 78 | return 'button'; 79 | case 'SECTION': 80 | return 'section'; 81 | case 'HEADER': 82 | return 'section'; 83 | default: 84 | break; 85 | } 86 | } 87 | 88 | export function cleanDOM (artboard) { 89 | const editables = Array.from(artboard.querySelectorAll('.is-editable')); 90 | const uploaders = Array.from(artboard.querySelectorAll('.uploader')); 91 | const stylers = Array.from(artboard.querySelectorAll('.styler')); 92 | 93 | editables.forEach((el) => { 94 | el.contentEditable = 'inherit'; 95 | el.classList.remove('is-editable'); 96 | }); 97 | uploaders.forEach((el) => { 98 | const input = el.querySelector(':scope > input'); 99 | const image = el.querySelector(':scope > img'); 100 | 101 | image.classList.add('add-full-width'); 102 | el.classList.remove('uploader'); 103 | input.remove(); 104 | }); 105 | stylers.forEach((styler) => { 106 | styler.remove(); 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /src/vuse.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash-es/merge'; 2 | import Section from './section'; 3 | import VuseBuilder from './components/VuseBuilder.vue'; 4 | import VuseRenderer from './components/VuseRenderer.vue'; 5 | import styler from './styler'; 6 | import mixin from './mixin'; 7 | import { cleanDOM } from './util'; 8 | 9 | let PLUGINS = []; 10 | let mixier = {}; 11 | const BUILDER_OPTIONS = { 12 | title: '', 13 | intro: true, 14 | sections: [], 15 | plugins: [], 16 | themes: [], 17 | columnsPrefix: { 18 | mobile: 'is-mobile-', 19 | tablet: 'is-tablet-', 20 | desktop: 'is-desktop-', 21 | widescreen: 'is-widescreen-', 22 | ultrawide: 'is-ultrawide-' 23 | } 24 | }; 25 | 26 | // To tell if it is installed or not 27 | let _Vue = null; 28 | 29 | class Vuse { 30 | constructor (options) { 31 | this.isEditing = true; 32 | this.isSorting = false; 33 | this.isRendered = false; 34 | this.title = options.title; 35 | this.intro = options.intro; 36 | this.sections = options.sections; 37 | this.columnsPrefix = options.columnsPrefix; 38 | this.themes = options.themes; 39 | this.components = {}; 40 | this.assets = { 41 | css: options.assets.css || 'dist/css/app.css' 42 | } 43 | this.installPlugins(); 44 | } 45 | 46 | /** 47 | * Creates and adds a new section to the list of sections. 48 | * @param {*} options 49 | */ 50 | add (options, position) { 51 | if (position !== undefined) { 52 | this.sections.splice(position, 0, new Section(options)); 53 | return; 54 | } 55 | this.sections.push(new Section(options)); 56 | } 57 | 58 | /** 59 | * Finds a section with the specified id. 60 | * 61 | * @param {String|Number} id 62 | */ 63 | find (id) { 64 | return this.sections.find(s => s.id === id); 65 | } 66 | 67 | /** 68 | * Removes a section with the specified id. 69 | * @param {String|Number} id 70 | */ 71 | remove (section) { 72 | const id = this.sections.findIndex(s => s.id === section.id); 73 | this.sections.splice(id, 1); 74 | section.destroy(); 75 | } 76 | 77 | /** 78 | * Removes a section with the specified id. 79 | * @param {String|Number} oldIndex 80 | * @param {String|Number} newIndex 81 | */ 82 | sort (oldIndex, newIndex) { 83 | const section = this.sections[oldIndex]; 84 | this.sections.splice(oldIndex, 1); 85 | this.sections.splice(newIndex, 0, section); 86 | } 87 | 88 | /** 89 | * Constructs a document fragment. 90 | */ 91 | outputFragment () { 92 | const frag = document.createDocumentFragment(); 93 | frag.appendChild(document.head.cloneNode(true)); 94 | frag.appendChild(this.rootEl.cloneNode(true)); 95 | 96 | return frag; 97 | } 98 | 99 | /** 100 | * clears the builder sections. 101 | */ 102 | clear () { 103 | const tempSections = this.sections; 104 | this.sections.forEach(section => section.destroy()); 105 | this.sections = []; 106 | return tempSections; 107 | } 108 | 109 | /** 110 | * Static helper for components registration pre-installation. 111 | * 112 | * @param {String} name 113 | * @param {Object} definition 114 | */ 115 | static component (name, definition) { 116 | // Just make a plugin that installs a component. 117 | Vuse.use((ctx) => { 118 | ctx.builder.component(name, definition); 119 | }); 120 | } 121 | 122 | /** 123 | * Acts as a mixin for subcomponents. 124 | * @param {Object} mixinObj 125 | */ 126 | static mix (mixinObj) { 127 | mixier = merge(mixier, mixinObj); 128 | } 129 | 130 | /** 131 | * Adds a component section to the builder and augments it with the styler. 132 | * @param {*} name 133 | * @param {*} definition 134 | */ 135 | component (name, definition) { 136 | // reoslve the component name automatically. 137 | if (typeof name === 'object') { 138 | definition = name; 139 | name = definition.name; 140 | } 141 | 142 | // if passed a plain object 143 | if (!definition.extend) { 144 | definition = _Vue.extend(definition); 145 | } 146 | 147 | this.components[name] = definition.extend({ 148 | directives: { styler: this.styler }, 149 | mixins: [this.mixin], 150 | components: mixier.components 151 | }); 152 | } 153 | 154 | /** 155 | * Installs added plugins. 156 | */ 157 | installPlugins () { 158 | PLUGINS.forEach((ctx) => { 159 | ctx.plugin({ builder: this, Vue: _Vue }, ctx.options); 160 | }); 161 | // reset to prevent duplications. 162 | PLUGINS = []; 163 | } 164 | 165 | static install (Vue, options = {}) { 166 | // already installed 167 | if (_Vue) return; 168 | 169 | _Vue = Vue; 170 | const builder = new Vuse(Object.assign({}, BUILDER_OPTIONS, options)); 171 | // configer assets output location 172 | Vue.util.defineReactive(builder, 'sections', builder.sections); 173 | Vue.util.defineReactive(builder, 'isEditing', builder.isEditing); 174 | Vue.util.defineReactive(builder, 'isSorting', builder.isSorting); 175 | const extension = { 176 | components: builder.components, 177 | beforeCreate () { 178 | this.$builder = builder; 179 | } 180 | }; 181 | 182 | // register the main components. 183 | Vue.component('VuseBuilder', Vue.extend(VuseBuilder).extend(extension)); 184 | Vue.component('VuseRenderer', Vue.extend(VuseRenderer).extend(extension)); 185 | } 186 | 187 | /** 188 | * The plugin to be installed with the builder. The function receives the installation context which 189 | * contains the builder instance and the Vue prototype. 190 | * 191 | * @param {Function} plugin 192 | * @param {Object} options 193 | */ 194 | static use (plugin, options = {}) { 195 | if (typeof plugin !== 'function') { 196 | return console.warn('Plugins must be a function'); 197 | } 198 | 199 | // append to the list of to-be installed plugins. 200 | PLUGINS.push({ plugin, options }); 201 | } 202 | 203 | set (data) { 204 | this.title = data.title !== undefined ? data.title : this.title; 205 | if (data.sections && Array.isArray(data.sections)) { 206 | this.sections = data.sections.map(section => { 207 | const sectionData = { 208 | name: section.name, 209 | schema: section.schema, 210 | data: section.data 211 | }; 212 | if (!sectionData.schema) { 213 | sectionData.schema = this.components[sectionData.name].options.$schema 214 | } 215 | 216 | return new Section(sectionData); 217 | }); 218 | } 219 | } 220 | 221 | /** 222 | * Outputs a JSON representation of the builder that can be used for rendering with the renderer component. 223 | */ 224 | toJSON () { 225 | return { 226 | title: this.title, 227 | sections: this.sections.map(s => ({ 228 | name: s.name, 229 | data: s.data 230 | })) 231 | }; 232 | } 233 | 234 | /** 235 | * Previews the created page in a seperate tap/window. 236 | */ 237 | preview () { 238 | const frag = this.outputFragment(); 239 | const artboard = frag.querySelector('#artboard'); 240 | const printPreview = window.open('about:blank', 'print_preview'); 241 | const printDocument = printPreview.document; 242 | cleanDOM(frag); 243 | printDocument.open(); 244 | printDocument.write( 245 | ` 246 | 247 | 248 | ${this.title} 249 | 250 | 251 | 252 | ${artboard.innerHTML} 253 | 254 | ` 255 | ); 256 | } 257 | 258 | /** 259 | * Exports the builder instance to a specified output. default is json. 260 | * 261 | * @param {String} method 262 | */ 263 | export (method = 'json') { 264 | if (method === 'pwa' || method === 'zip') { 265 | if (typeof this.download === 'function') { 266 | return this.download(this.assets); 267 | } 268 | 269 | return console.warn('You do not have the zip plugin installed.'); 270 | } 271 | 272 | if (method === 'preview') { 273 | return this.preview(); 274 | } 275 | 276 | return this.toJSON(); 277 | } 278 | }; 279 | 280 | // use the plugin API to add the styler and mixin functionalities. 281 | Vuse.use(styler); 282 | Vuse.use(mixin); 283 | 284 | export default Vuse; 285 | --------------------------------------------------------------------------------