```
================================================
FILE: docs/getting-started.md
================================================
# getting-started
## What is this
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.
The user/developer can then export the builder for usability in multiple formats, the following are the officially supported ones:
- `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.
- `preview` opens a new page without the editable logic in new tab to see the end result.
- `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.
The builder is just a Vue plugin, so you can integrate this into your own projects without needing to create separate bundles for it.
## Installation
### Package managers
First step is to install it using `yarn` or `npm`:
```bash
npm install vuse
# or use yarn
yarn add vuse
```
### CDNs
Or add it as a script tag in your projects.
- [unpkg](https://unpkg.com/vuse)
```html
```
### Usage
::: tip
If you added it using a script tag, you can skip this section. as it will be auto-installed for you
:::
```js
import Builder from 'vuse';
Vue.use(Builder);
```
You can start using the `b-builder` component to build things now.
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.
================================================
FILE: docs/section.md
================================================
# Section
A section is the building block of the page, below is an example of a header section.
::: tip
Examples use [pug](https://pugjs.org) template language to make it easier to work with templates.
:::
```pug
section.header(
v-styler="$sectionData.classes"
:class="[{'is-editable': $builder.isEditing}, $sectionData.classes]"
)
.container
.grid
.column.is-desktop-6.add-center-vertical
h3.header-title(
:class="{'is-editable': $builder.isEditing}"
v-html="$sectionData.title"
v-styler="$sectionData.title"
)
p.header-content(
:class="{'is-editable': $builder.isEditing}"
v-html="$sectionData.content"
v-styler="$sectionData.content"
)
a.button(
:class="[{'is-editable': $builder.isEditing}, $sectionData.button.classes]"
:href="$sectionData.button.href"
v-html="$sectionData.button.text"
v-styler="$sectionData.button"
)
.column.is-desktop-6
uploader(
class="header-image"
path="$sectionData.images[0]"
)
```
Each section has several elements that can be edited. Section data are stored in `$sectionData` object which is reactive.
## Adding the ability to edit elements in a section
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}"`
1. Add [`v-styler`](https://github.com/baianat/builder#v-styler) directive to the element
1. Bind the element’s innerHTML with its equivalent data e.g. `v-html="$sectionData.button.text"`
1. If you have any other data that `v-styler` changes, you have to bind it too. e.g. `:href="$sectionData.button.href"`
Putting it all together
```html
```
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.
```html
```
## Using the section
Until now, we have only been creating our section component, we now need to introduce it to our builder so it can use it:
```js
import Builder from 'vuse';
import section from './components/sections/section';
// Builder has Vue-like API when registering components.
Builder.component(section);
Vue.use(Builder);
new Vue({
el: '#app'
});
```
```html
```
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.
================================================
FILE: docs/styler.md
================================================
# Styler
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.
To tell styler which variable to update, you pass it as directive expression e.g. `v-styler="$sectionData.button"`
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.
If you want to explicitly specify the type, you can pass it as a directive modifier e.g. `v-styler.button="$sectionData.button"`.
## How to use
coming soon...
================================================
FILE: package.json
================================================
{
"name": "vuse",
"version": "0.1.1",
"description": "Vue.js Page Builder",
"author": "Abdelrahman Ismail ",
"module": "dist/vuse.esm.js",
"unpkg": "dist/vuse.min.js",
"main": "dist/vuse.js",
"scripts": {
"dev": "webpack-dev-server --hot --inline --config ./demo/webpack.config.js",
"build": "cross-env NODE_ENV=production node scripts/build.js",
"build:demo": "cross-env NODE_ENV=production webpack --config ./demo/webpack.config.js",
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs",
"docs:deploy": "scripts/deploy.sh",
"lint": "eslint ./src --fix"
},
"devDependencies": {
"@babel/core": "^7.0.0-rc.1",
"@babel/plugin-proposal-class-properties": "^7.0.0-rc.1",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0-rc.1",
"@babel/preset-env": "^7.0.0-rc.1",
"babel-loader": "^8.0.0-beta",
"chalk": "^2.4.1",
"clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^4.5.1",
"cross-env": "^5.0.5",
"css-loader": "^1.0.0",
"eslint": "^5.3.0",
"eslint-config-standard": "^11.0.0",
"eslint-loader": "^2.1.0",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.7.1",
"file-loader": "^1.1.11",
"filesize": "^3.6.1",
"friendly-errors-webpack-plugin": "^1.6.1",
"gzip-size": "^5.0.0",
"html-webpack-plugin": "^4.0.0-alpha",
"mini-css-extract-plugin": "^0.4.4",
"mkdirp": "^0.5.1",
"progress-bar-webpack-plugin": "^1.10.0",
"pug": "^2.0.0-rc.3",
"pug-plain-loader": "^1.0.0",
"rollup": "^0.64.1",
"rollup-plugin-buble": "^0.19.2",
"rollup-plugin-commonjs": "^9.1.5",
"rollup-plugin-css-only": "^0.4.0",
"rollup-plugin-node-resolve": "^3.0.0",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-uglify": "^4.0.0",
"rollup-plugin-vue": "^4.3.2",
"style-loader": "^0.22.1",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.1",
"stylus-relative-loader": "^3.4.0",
"util": "^0.11.0",
"vue": "^2.5.17",
"vue-loader": "^15.3.0",
"vue-template-compiler": "^2.5.17",
"vuepress": "^0.14.8",
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5"
},
"dependencies": {
"@baianat/base.framework": "^2.0.0-beta.0",
"intersection-observer": "^0.5.0",
"jszip": "^3.1.4",
"lodash-es": "^4.17.4",
"popper.js": "^1.14.4",
"save-as": "^0.1.8",
"sortablejs": "^1.6.1",
"vue-server-renderer": "2.5.17"
},
"license": "MIT",
"files": [
"dist/*.js",
"dist/*.css"
],
"keywords": [
"page-builder",
"vuejs",
"ES6"
],
"maintainers": [
{
"name": "Abdelrahman Awad",
"email": "logaretm1@gmail.com"
},
{
"name": "Abdelrahman Ismail",
"email": "abdelrahman3d@gmail.com"
}
]
}
================================================
FILE: scripts/build.js
================================================
const chalk = require('chalk');
const mkdirpNode = require('mkdirp');
const { promisify } = require('util');
const { rollup } = require('rollup');
const { paths, configs, utils } = require('./config');
const mkdirp = promisify(mkdirpNode);
async function buildConfig (build) {
await mkdirp(paths.dist);
const bundleName = build.output.file.replace(paths.dist, '');
console.log(chalk.cyan(`📦 Generating ${bundleName}...`));
const bundle = await rollup(build.input);
await bundle.write(build.output);
console.log(chalk.green(`👍 ${bundleName} ${utils.stats({ path: build.output.file })}`));
}
async function build () {
await Promise.all(Object.keys(configs).map(key => {
return buildConfig(configs[key]).catch(err => {
console.log(err);
});
}));
process.exit(0);
}
build();
================================================
FILE: scripts/config.js
================================================
const path = require('path');
const fs = require('fs');
const replace = require('rollup-plugin-replace');
const vue = require('rollup-plugin-vue').default;
const resolve = require('rollup-plugin-node-resolve');
const css = require('rollup-plugin-css-only');
const buble = require('rollup-plugin-buble');
const commonjs = require('rollup-plugin-commonjs');
const filesize = require('filesize');
const gzipSize = require('gzip-size');
const { uglify } = require('rollup-plugin-uglify');
const version = process.env.VERSION || require('../package.json').version;
const common = {
banner:
`/**
* Vuse v${version}
* (c) ${new Date().getFullYear()} Baianat
* @license MIT
*/`,
paths: {
input: path.join(__dirname, '../src/index.js'),
src: path.join(__dirname, '../src/'),
dist: path.join(__dirname, '../dist/')
},
builds: {
umd: {
file: 'vuse.js',
format: 'umd',
name: 'vuse',
env: 'development'
},
umdMin: {
file: 'vuse.min.js',
format: 'umd',
name: 'vuse',
env: 'production'
},
esm: {
input: path.join(__dirname, '../src/index.esm.js'),
file: 'vuse.esm.js',
format: 'es'
}
}
};
function genConfig (options) {
const config = {
description: '',
input: {
input: options.input || common.paths.input,
plugins: [
commonjs(),
replace({ __VERSION__: version }),
css(),
vue({ css: false }),
resolve(),
buble()
]
},
output: {
banner: common.banner,
name: options.name,
format: options.format,
file: path.join(common.paths.dist, options.file)
}
};
if (options.env) {
config.input.plugins.unshift(replace({
'process.env.NODE_ENV': JSON.stringify(options.env)
}));
}
if (options.env === 'production') {
config.input.plugins.push(uglify());
}
return config;
};
const configs = Object.keys(common.builds).reduce((prev, key) => {
prev[key] = genConfig(common.builds[key]);
return prev;
}, {});
module.exports = {
configs,
uglifyOptions: common.uglifyOptions,
paths: common.paths,
utils: {
stats ({ path }) {
const code = fs.readFileSync(path);
const { size } = fs.statSync(path);
const gzipped = gzipSize.sync(code);
return `| Size: ${filesize(size)} | Gzip: ${filesize(gzipped)}`;
}
}
};
================================================
FILE: scripts/deploy.sh
================================================
#!/usr/bin/env sh
set -e
npm run docs:build
cd docs/.vuepress/dist
git init
git add -A
git commit -m 'deploy'
git push -f git@github.com:baianat/vuse.git master:gh-pages
cd -
================================================
FILE: src/components/VuseBuilder.vue
================================================
div
div#artboard.artboard(
ref="artboard"
:class="{ 'is-sorting': $builder.isSorting, 'is-editable': $builder.isEditing }"
)
component(v-for='section in $builder.sections'
:is='section.name'
:key='section.id'
:id='section.id'
)
.controller
.controller-intro(v-if="showIntro && !this.$builder.sections.length")
label(for="projectName") Hello, start your project
input.controller-input(
id="projectName"
placeholder="project name"
v-model="title"
)
template(v-if="themes")
.controller-themes
button.controller-theme(
v-for="theme in themes"
@click="addTheme(theme)"
)
| {{ theme.name }}
.controller-panel
button.controller-button.is-green(
tooltip-position="top"
tooltip="export"
@click="submit"
)
VuseIcon(name='download')
button.controller-button.is-red(
v-if="!tempSections"
tooltip-position="top"
tooltip="clear sections"
@click="clearSections"
)
VuseIcon(name='trash')
button.controller-button.is-gray(
v-if="tempSections"
tooltip-position="top"
tooltip="undo"
@click="undo"
)
VuseIcon(name='undo')
button.controller-button.is-blue(
tooltip-position="top"
tooltip="sorting"
:class="{ 'is-red': $builder.isSorting }"
@click="toggleSort"
)
VuseIcon(name='sort')
button.controller-button.is-blue(
tooltip-position="top"
tooltip="add section"
:class="{ 'is-red': listShown, 'is-rotated': listShown }"
:disabled="!$builder.isEditing"
@click="newSection"
)
VuseIcon(name='plus')
ul.menu(:class="{ 'is-visiable': listShown }" ref="menu")
li.menu-group(v-for="(group, name) in groups" v-if="group.length")
.menu-header(@click="toggleGroupVisibility")
span.menu-title {{ name }}
span.menu-icon
VuseIcon(name='arrowDown')
.menu-body
template(v-for="section in group")
a.menu-element(
@click="addSection(section)"
@drag="currentSection = section"
)
img.menu-elementImage(v-if="section.cover" :src="section.cover")
span.menu-elementTitle {{ section.name }}
================================================
FILE: src/components/VuseIcon.js
================================================
const icons = {
plus: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z',
tic: 'M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z',
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',
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',
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',
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',
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',
italic: 'M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z',
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',
center: 'M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z',
left: 'M15 15H3v2h12v-2zm0-8H3v2h12V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z',
right: 'M3 21h18v-2H3v2zm6-4h12v-2H9v2zm-6-4h18v-2H3v2zm6-4h12V7H9v2zM3 3v2h18V3H3z',
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',
align: 'M3 21h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18V7H3v2zm0-6v2h18V3H3z',
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',
section: 'M10 18h5v-6h-5v6zm-6 0h5V5H4v13zm12 0h5v-6h-5v6zM10 5v6h11V5H10z',
arrowDown: 'M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z',
arrowRight: 'M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z',
arrowLeft: 'M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z',
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',
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',
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',
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',
download: 'M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z',
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',
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'
};
export default {
functional: true,
props: {
name: {
type: String,
required: true,
validator: (val) => {
if (!(val in icons) && process.env.NODE_ENV !== 'production') {
console.warn(`Invalid icon name "${val}"`);
return false;
}
return true;
}
}
},
render (h, { props }) {
const path = h('path', {
attrs: {
d: icons[props.name]
}
});
return h(
'svg',
{
attrs: {
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg',
class: 'vuse-icon',
viewBox: '0 0 24 24'
}
},
[path]
);
}
};
================================================
FILE: src/components/VuseRenderer.vue
================================================
#artboard.artboard
component(v-for='section in $builder.sections'
:is='section.name'
:key='section.id'
:id='section.id'
)
================================================
FILE: src/components/VuseStyler.vue
================================================
.styler(
ref="styler"
id="styler"
v-if="$builder.isEditing"
:class="{ 'is-visible': isVisible }"
@click.stop=""
)
ul.styler-list
li(v-if="type === 'button' || type === 'section'")
button.styler-button(@click="updateOption('colorer')")
VuseIcon(name='palettes')
li(v-if="type === 'button'")
button.styler-button(@click="updateOption('link')")
VuseIcon(name='link')
li(v-if="type === 'header' || type === 'section'")
button.styler-button(@click="removeSection")
VuseIcon(name='trash')
template(v-if="type === 'text'")
li: button.styler-button(@click="updateOption('textColor')")
VuseIcon(name='palettes')
li: button.styler-button(@click="updateOption('align')")
VuseIcon(name='align')
li: button.styler-button(@click="updateOption('textStyle')")
VuseIcon(name='textStyle')
template(v-if="type === 'grid'")
li: button.styler-button(@click="selectDevice('mobile')")
VuseIcon(name='mobile')
//- li: button.styler-button(@click="selectDevice('tablet')")
//- VuseIcon(name='tablet')
li: button.styler-button(@click="selectDevice('desktop')")
VuseIcon(name='laptop')
//- li: button.styler-button(@click="selectDevice('widescreen')")
//- VuseIcon(name='monitor')
ul.styler-list
li(v-if="currentOption === 'colorer'")
ul.colorer
li(v-for="color in colors")
input(
type="radio"
:id="`color${color.charAt(0).toUpperCase() + color.slice(1)}`"
name="colorer"
:value="color"
v-model="colorerColor"
)
li(v-if="currentOption === 'textColor'")
ul.colorer
li(v-for="(color, index) in colors")
input(
type="radio"
:id="`color${color.charAt(0).toUpperCase() + color.slice(1)}`"
name="colorer"
:value="textColors[index]"
v-model="textColor"
)
li(v-if="currentOption === 'link'")
.input-group.is-rounded.has-itemAfter.is-primary
input.input(type="text" placeholder="type your link" v-model="url")
button.button(@click="addLink")
VuseIcon(name='link')
li(v-if="currentOption === 'align'")
ul.align
li: button.styler-button(@click="execute('justifyleft')")
VuseIcon(name='left')
li: button.styler-button(@click="execute('justifycenter')")
VuseIcon(name='center')
li: button.styler-button(@click="execute('justifyright')")
VuseIcon(name='right')
li(v-if="currentOption === 'textStyle'")
ul.align
li: button.styler-button(@click="execute('bold')")
VuseIcon(name='bold')
li: button.styler-button(@click="execute('italic')")
VuseIcon(name='italic')
li: button.styler-button(@click="execute('underline')")
VuseIcon(name='underline')
li(v-if="currentOption === 'columnWidth'")
ul.align
li: button.styler-button(@click="gridValue--")
VuseIcon(name='arrowLeft')
li: input(type="number" min="0" max="12" v-model="gridValue").styler-input
li: button.styler-button(@click="gridValue++")
VuseIcon(name='arrowRight')
================================================
FILE: src/index.esm.js
================================================
import Vuse from './vuse';
import * as types from './types';
const version = '__VERSION__';
// Auto install if Vue is defined globally.
if (typeof Vue !== 'undefined') {
// eslint-disable-next-line
Vue.use(Builder);
}
export {
Vuse,
types,
version
};
export default Vuse;
================================================
FILE: src/index.js
================================================
import Vuse from './vuse';
import * as types from './types';
// Auto install if Vue is defined globally.
if (typeof Vue !== 'undefined') {
// eslint-disable-next-line
Vue.use(Builder);
}
Vuse.version = '__VERSION__';
Vuse.types = types;
export default Vuse;
================================================
FILE: src/mixin.js
================================================
function installMixin ({ builder }) {
builder.mixin = {
provide: function providesBuilder () {
const provides = {};
if (this.$builder) {
provides.$builder = this.$builder;
}
if (this.$section) {
provides.$section = this.$section;
}
return provides;
},
beforeCreate () {
this.$builder = builder;
if (!this.$options.propsData || this.$options.propsData.id === undefined) {
return;
}
this.$section = this.$builder.find(this.$options.propsData.id);
this.$options.computed = {
$sectionData: function getSectionData () {
return this.$section.data;
},
gridClasses: function getGridClasses () {
return this.$sectionData.columns.map(column => {
return Object.keys(column.grid).map(device => {
if (!column.grid[device]) {
return '';
}
const prefix = this.$builder.columnsPrefix[device]
return `${prefix}${column.grid[device]}`;
});
})
}
}
},
updated () {
Array.from(this.$el.querySelectorAll('[contentEditable]')).forEach((el) => {
el.contentEditable = this.$builder.isEditing;
});
}
};
};
export default installMixin;
================================================
FILE: src/plugins/pwa.js
================================================
import JSZip from 'jszip';
import saveAs from 'save-as';
import { getImageBlob, cleanDOM } from '../../src/util';
/**
* Adds a service worker that caches the static assets.
*/
function createSW (output, { images = [] } = {}) {
output.file('sw.js', `
const staticCacheName = 'bbuilder-static-v1';
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(staticCacheName).then(function(cache) {
return cache.addAll([
'/',
'/assets/js/main.js',
${images.map(i => "'/assets/img/" + i.name + "'").join(',').trim(',')}
]);
})
);
});
function serveAsset(request) {
return caches.open(staticCacheName).then(function(cache) {
return cache.match(request).then(function(response) {
if (response) return response;
return fetch(request).then(function(networkResponse) {
cache.put(request, networkResponse.clone());
return networkResponse;
});
});
});
}
self.addEventListener('fetch', function(event) {
const requestUrl = new URL(event.request.url);
if (requestUrl.origin === location.origin) {
if (requestUrl.pathname === '/') {
event.respondWith(caches.match('/'));
return;
}
if (requestUrl.pathname.startsWith('/assets/')) {
event.respondWith(serveAsset(event.request));
return;
}
}
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
`);
const scripts = output.folder('assets/js');
scripts.file('main.js', `
function registerSW () {
if (!navigator.serviceWorker) return;
navigator.serviceWorker.register('/sw.js').then(function (reg) {
console.log('SW registered!');
});
}
registerSW();
`);
}
/**
* Adds some PWA features.
*/
function createPWA (output, payload) {
createSW(output, payload);
}
function download (assets) {
const frag = this.outputFragment();
const images = Array.from(frag.querySelectorAll('img'));
const artboard = frag.querySelector('#artboard');
const title = document.title;
const zip = new JSZip();
const output = zip.folder('project');
const imgFolder = output.folder('assets/img');
const cssFolder = output.folder('assets/css');
Promise.all(images.map((image) => {
const imageLoader = getImageBlob(image.src);
return imageLoader.then((img) => {
imgFolder.file(img.name, img.blob, { base64: true });
image.setAttribute('src', `assets/img/${img.name}`);
return img;
});
})).then(images => {
createPWA(output, { images });
}).then(() => {
return new Promise((resolve, reject) => {
const assetsClient = new XMLHttpRequest();
assetsClient.open('GET', assets.css);
assetsClient.onload = function () {
resolve(this.response);
}
assetsClient.send(null);
}).then((content) => {
cssFolder.file('app.css', content);
return content;
});
}).then(() => {
cleanDOM(frag);
output.file('index.html',
`
${title}
${artboard.innerHTML}
`);
zip.generateAsync({ type: 'blob' }).then((blob) => {
saveAs(blob, 'project.zip');
});
});
}
export default function install ({ builder }) {
builder.download = download;
};
================================================
FILE: src/plugins/scrolling.js
================================================
import 'intersection-observer';
const callback = (entries, observer) => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0.5) {
entry.target.classList.add('is-active');
entry.target.classList.remove('is-inactive');
return;
}
entry.target.classList.add('is-inactive');
entry.target.classList.remove('is-active');
});
}
const observer = new IntersectionObserver(callback, {
root: null,
rootMargin: '0px',
threshold: [0.1, 0.5, 0.9, 1]
});
const scrolling = (rootEl) => {
if (!rootEl) return;
let sections = Array.from(rootEl.querySelectorAll('section'));
sections.forEach(section => {
section.classList.add('is-inactive');
observer.observe(section);
});
}
export default function install ({ builder }) {
builder.scrolling = scrolling;
};
================================================
FILE: src/section.js
================================================
import getPath from 'lodash-es/get';
import toPath from 'lodash-es/toPath';
import Seeder from './seeder';
const SECTION_OPTIONS = {
name: null,
schema: {}
};
let counter = 0;
export default class Section {
constructor (options) {
this.id = counter++;
options = Object.assign({}, SECTION_OPTIONS, options);
this.name = options.name;
this.schema = options.schema;
this.data = options.data || Seeder.seed(options.schema);
this.stylers = [];
}
set (name, value) {
const path = toPath(name);
const prop = path.pop();
path.shift();
const obj = path.length === 0 ? this.data : getPath(this.data, path);
if (typeof value === 'function') {
value(obj[prop]);
return;
}
obj[prop] = value;
}
get (name) {
const path = toPath(name);
const prop = path.pop();
path.shift();
const obj = path.length === 0 ? this.data : getPath(this.data, path);
return obj[prop];
}
destroy () {
this.stylers.forEach(styler => styler.$destroy())
}
};
================================================
FILE: src/seeder.js
================================================
import * as types from './types';
import { isObject } from './util';
const ASSETS_DIR = '.';
const data = new Map([
[types.Title, 'Awesome title'],
[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.'],
[types.Avatar, `${ASSETS_DIR}/img/avatar.png`],
[types.Logo, `${ASSETS_DIR}/img/google.svg`],
[types.Link, 'http://example.com'],
[types.Image, `${ASSETS_DIR}/img/baianat.png`],
[types.ClassList, () => []],
[types.Button, () => ({ text: 'Click Me!', classes: [], href: 'http://example.com' })],
[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.'],
[types.Grid, () => ({mobile: '', tablet: '', desktop: '', widescreen: ''})],
[Number, 100],
[String, 'This is pretty neat']
]);
export default class Seeder {
// Seeds values using a schema.
static seed (schema) {
if (isObject(schema)) {
return Object.keys(schema).reduce((values, key) => {
values[key] = Seeder.seed(schema[key]);
return values;
}, {});
} else if (Array.isArray(schema)) {
return schema.map(s => {
return Seeder.seed(s)
});
}
let value = data.get(schema);
if (value === undefined) {
value = schema;
}
return typeof value === 'function' ? value() : value;
}
};
================================================
FILE: src/styler.js
================================================
import Styler from './components/VuseStyler.vue';
import { getTypeFromTagName, getTypeFromSchema } from './util';
function installStyler ({ builder, Vue }) {
const StylerInstance = Vue.extend(Styler).extend({
beforeCreate () {
this.$builder = builder;
}
});
builder.styler = {
inserted (el, binding, vnode) {
const newNode = document.createElement('div');
const section = vnode.context.$section;
const rootApp = vnode.context.$root.$el;
rootApp.appendChild(newNode);
el.classList.add('is-editable');
section.stylers.push(new StylerInstance({
propsData: {
el,
section: section,
type: binding.arg || getTypeFromSchema(binding.expression, section.schema) || getTypeFromTagName(el.tagName),
name: binding.expression
}
}).$mount(newNode));
}
};
};
export default installStyler;
================================================
FILE: src/stylus/_app.styl
================================================
@import variables
@import colors
.vuse-icon
display: block
width: 20px
height: 20px
$floatHover
cursor: pointer
box-shadow: 0 14px 28px alpha($black, 0.125), 0 10px 10px alpha($black, 0.1)
================================================
FILE: src/stylus/colors.styl
================================================
/*
* Color theme
*/
$magenta ?= #eb008b
$blue ?= #0072FF
$cyan ?= #00d4f0
$green ?= #18d88b
$yellow ?= #ffdd57
$orange ?= #ffa557
$red ?= #ff3d3d
$purple ?= #a324ea
/*
* Graysacle
*/
$black ?= #000
$dark ?= #323c47
$gray ?= #c1c1c1
$gray-dark ?= darken($gray, 10%)
$gray-light ?= lighten($gray, 10%)
$light ?= #f5f5f5
$white ?= #fff
================================================
FILE: src/stylus/variables.styl
================================================
$flexCenter
display: flex
justify-content: center
align-items: center
================================================
FILE: src/types.js
================================================
export class Avatar {};
export class Title {};
export class Text {};
export class Logo {};
export class Image {};
export class Quote {};
export class Link {};
export class ClassList {};
export class Button {};
export class Grid { };
================================================
FILE: src/util.js
================================================
import getPath from 'lodash/get';
import * as types from './types';
export function isObject (obj) {
return obj && typeof obj === 'object' && obj !== null && !Array.isArray(obj);
};
export function isParentTo (target, parent) {
let currentNode = target;
while (currentNode !== null) {
if (currentNode === parent) return true;
currentNode = currentNode.parentNode;
}
return false;
}
/**
*
* @param {String} target
* @param {Object} schema
*/
export function getTypeFromSchema (target, schema) {
const tempTarget = target.split('.');
tempTarget.shift();
const value = getPath(schema, tempTarget.join('.'));
if (value === types.Grid) return 'grid';
if (value === types.Text) return 'text';
if (value === types.Title) return 'text';
if (value === types.Button) return 'button';
if (value === types.ClassList) return 'section';
if (value === String) return 'text';
if (value === Number) return 'text';
return null;
}
export function getImageBlob (URL) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', URL);
xhr.responseType = 'blob';
xhr.onload = function () {
const imageBlob = this.response;
const fileType = this.response.type.split('/')[1].split('+')[0];
const randomNumber = new Date().getUTCMilliseconds();
const filename = `image-${randomNumber}.${fileType}`;
resolve({ blob: imageBlob, name: filename });
}
xhr.send(null);
});
}
export function getTypeFromTagName (tagName) {
tagName = tagName.toUpperCase();
switch (tagName) {
case 'H1':
return 'text';
case 'H2':
return 'text';
case 'H3':
return 'text';
case 'H4':
return 'text';
case 'H5':
return 'text';
case 'H6':
return 'text';
case 'P':
return 'text';
case 'B':
return 'text';
case 'SPAN':
return 'text';
case 'BUTTON':
return 'button';
case 'A':
return 'button';
case 'SECTION':
return 'section';
case 'HEADER':
return 'section';
default:
break;
}
}
export function cleanDOM (artboard) {
const editables = Array.from(artboard.querySelectorAll('.is-editable'));
const uploaders = Array.from(artboard.querySelectorAll('.uploader'));
const stylers = Array.from(artboard.querySelectorAll('.styler'));
editables.forEach((el) => {
el.contentEditable = 'inherit';
el.classList.remove('is-editable');
});
uploaders.forEach((el) => {
const input = el.querySelector(':scope > input');
const image = el.querySelector(':scope > img');
image.classList.add('add-full-width');
el.classList.remove('uploader');
input.remove();
});
stylers.forEach((styler) => {
styler.remove();
});
}
================================================
FILE: src/vuse.js
================================================
import merge from 'lodash-es/merge';
import Section from './section';
import VuseBuilder from './components/VuseBuilder.vue';
import VuseRenderer from './components/VuseRenderer.vue';
import styler from './styler';
import mixin from './mixin';
import { cleanDOM } from './util';
let PLUGINS = [];
let mixier = {};
const BUILDER_OPTIONS = {
title: '',
intro: true,
sections: [],
plugins: [],
themes: [],
columnsPrefix: {
mobile: 'is-mobile-',
tablet: 'is-tablet-',
desktop: 'is-desktop-',
widescreen: 'is-widescreen-',
ultrawide: 'is-ultrawide-'
}
};
// To tell if it is installed or not
let _Vue = null;
class Vuse {
constructor (options) {
this.isEditing = true;
this.isSorting = false;
this.isRendered = false;
this.title = options.title;
this.intro = options.intro;
this.sections = options.sections;
this.columnsPrefix = options.columnsPrefix;
this.themes = options.themes;
this.components = {};
this.assets = {
css: options.assets.css || 'dist/css/app.css'
}
this.installPlugins();
}
/**
* Creates and adds a new section to the list of sections.
* @param {*} options
*/
add (options, position) {
if (position !== undefined) {
this.sections.splice(position, 0, new Section(options));
return;
}
this.sections.push(new Section(options));
}
/**
* Finds a section with the specified id.
*
* @param {String|Number} id
*/
find (id) {
return this.sections.find(s => s.id === id);
}
/**
* Removes a section with the specified id.
* @param {String|Number} id
*/
remove (section) {
const id = this.sections.findIndex(s => s.id === section.id);
this.sections.splice(id, 1);
section.destroy();
}
/**
* Removes a section with the specified id.
* @param {String|Number} oldIndex
* @param {String|Number} newIndex
*/
sort (oldIndex, newIndex) {
const section = this.sections[oldIndex];
this.sections.splice(oldIndex, 1);
this.sections.splice(newIndex, 0, section);
}
/**
* Constructs a document fragment.
*/
outputFragment () {
const frag = document.createDocumentFragment();
frag.appendChild(document.head.cloneNode(true));
frag.appendChild(this.rootEl.cloneNode(true));
return frag;
}
/**
* clears the builder sections.
*/
clear () {
const tempSections = this.sections;
this.sections.forEach(section => section.destroy());
this.sections = [];
return tempSections;
}
/**
* Static helper for components registration pre-installation.
*
* @param {String} name
* @param {Object} definition
*/
static component (name, definition) {
// Just make a plugin that installs a component.
Vuse.use((ctx) => {
ctx.builder.component(name, definition);
});
}
/**
* Acts as a mixin for subcomponents.
* @param {Object} mixinObj
*/
static mix (mixinObj) {
mixier = merge(mixier, mixinObj);
}
/**
* Adds a component section to the builder and augments it with the styler.
* @param {*} name
* @param {*} definition
*/
component (name, definition) {
// reoslve the component name automatically.
if (typeof name === 'object') {
definition = name;
name = definition.name;
}
// if passed a plain object
if (!definition.extend) {
definition = _Vue.extend(definition);
}
this.components[name] = definition.extend({
directives: { styler: this.styler },
mixins: [this.mixin],
components: mixier.components
});
}
/**
* Installs added plugins.
*/
installPlugins () {
PLUGINS.forEach((ctx) => {
ctx.plugin({ builder: this, Vue: _Vue }, ctx.options);
});
// reset to prevent duplications.
PLUGINS = [];
}
static install (Vue, options = {}) {
// already installed
if (_Vue) return;
_Vue = Vue;
const builder = new Vuse(Object.assign({}, BUILDER_OPTIONS, options));
// configer assets output location
Vue.util.defineReactive(builder, 'sections', builder.sections);
Vue.util.defineReactive(builder, 'isEditing', builder.isEditing);
Vue.util.defineReactive(builder, 'isSorting', builder.isSorting);
const extension = {
components: builder.components,
beforeCreate () {
this.$builder = builder;
}
};
// register the main components.
Vue.component('VuseBuilder', Vue.extend(VuseBuilder).extend(extension));
Vue.component('VuseRenderer', Vue.extend(VuseRenderer).extend(extension));
}
/**
* The plugin to be installed with the builder. The function receives the installation context which
* contains the builder instance and the Vue prototype.
*
* @param {Function} plugin
* @param {Object} options
*/
static use (plugin, options = {}) {
if (typeof plugin !== 'function') {
return console.warn('Plugins must be a function');
}
// append to the list of to-be installed plugins.
PLUGINS.push({ plugin, options });
}
set (data) {
this.title = data.title !== undefined ? data.title : this.title;
if (data.sections && Array.isArray(data.sections)) {
this.sections = data.sections.map(section => {
const sectionData = {
name: section.name,
schema: section.schema,
data: section.data
};
if (!sectionData.schema) {
sectionData.schema = this.components[sectionData.name].options.$schema
}
return new Section(sectionData);
});
}
}
/**
* Outputs a JSON representation of the builder that can be used for rendering with the renderer component.
*/
toJSON () {
return {
title: this.title,
sections: this.sections.map(s => ({
name: s.name,
data: s.data
}))
};
}
/**
* Previews the created page in a seperate tap/window.
*/
preview () {
const frag = this.outputFragment();
const artboard = frag.querySelector('#artboard');
const printPreview = window.open('about:blank', 'print_preview');
const printDocument = printPreview.document;
cleanDOM(frag);
printDocument.open();
printDocument.write(
`
${this.title}
${artboard.innerHTML}
`
);
}
/**
* Exports the builder instance to a specified output. default is json.
*
* @param {String} method
*/
export (method = 'json') {
if (method === 'pwa' || method === 'zip') {
if (typeof this.download === 'function') {
return this.download(this.assets);
}
return console.warn('You do not have the zip plugin installed.');
}
if (method === 'preview') {
return this.preview();
}
return this.toJSON();
}
};
// use the plugin API to add the styler and mixin functionalities.
Vuse.use(styler);
Vuse.use(mixin);
export default Vuse;