GrapesJS Custom Style Manager
by artur_arseniev
HTML
<script src="https://unpkg.com/grapesjs"></script>
<link rel="stylesheet" href="https://unpkg.com/grapesjs/dist/css/grapes.min.css">
<script src="https://unpkg.com/grapesjs-blocks-basic"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.min.css">
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.min.js"></script>
<div id="gjs">
<div style="padding: 25px">Custom Style Manager</div>
</div>
<div style="display: none;">
<!-- Vue app -->
<div class="style-manager">
<v-app>
<v-main>
<v-expansion-panels accordion multiple>
<v-expansion-panel v-for="sector in sectors" :key="sector.getId()">
<v-expansion-panel-header>
{{ sector.getName() }}
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-row>
<property-field v-for="prop in sector.getProperties()" :key="prop.getId() + prop.canClear()" :prop="prop"/>
</v-row>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-main>
</v-app>
</div>
<!-- Property Field template -->
<div id="property-field" style="display: none;">
<v-col :class="['py-0 px-1 mb-1', prop.isFull() && 'mb-3']" :cols="prop.get('full') ? '12' : '6'">
<v-row :class="labelCls">
<v-col cols="auto pr-0">{{ prop.getLabel() }}</v-col>
<v-col cols="auto" v-if="prop.canClear()">
<v-icon @click="prop.clear()" color="indigo accent-1" small>mdi-close</v-icon>
</v-col>
</v-row>
<div v-if="propType === 'number'">
<v-text-field :placeholder="defValue" :value="inputValue" @change="handleChange" outlined dense/>
</div>
<div v-else-if="propType === 'radio'">
...
CSS
body, html {
margin: 0;
height: 100%;
}
.style-manager {
font-size: 12px;
}
.sm-input-color {
width: 16px !important;
height: 15px !important;
opacity: 0 !important;
}
.sm-type-cmp {
background-color: rgba(255,255,255,.05);
border: 1px solid rgba(255,255,255,.25);
border-radius: 3px;
position: relative;
min-height: 45px;
}
.sm-add-layer {
position: absolute !important;
top: -20px;
right: 12px;
}
.sm-layer + .sm-layer {
border-top: 1px solid rgba(255,255,255,.25);
}
.sm-layer-prv,
.sm-btn-prv {
width: 16px;
height: 16px;
border-radius: 3px;
display: inline-block;
}
.sm-layer-prv--text-shadow::after {
color: #000;
content: "T";
font-weight: 900;
font-size: 10px;
text-align: center;
display: block;
}
.gjs-pn-views-container, .gjs-pn-views {
width: 280px;
}
.gjs-pn-options {
right: 280px;
}
.gjs-cv-canvas {
width: calc(100% - 280px);
}
/* Vuetify overrides */
.v-application {
background: transparent !important;
}
.v-application--wrap {
min-height: auto;
}
.v-input__slot {
font-size: 12px;
min-height: 10px !important;
}
.v-select__selections {
flex-wrap: nowrap;
}
.v-text-field .v-input__slot {
padding: 0 10px !important;
}
.v-input--selection-controls {
margin-top: 0;
}
.v-text-field__details, .v-messages {
display: none;
}
Babel + JSX
const editor = grapesjs.init({
container: '#gjs',
fromElement: true,
height: '100%',
storageManager: false,
noticeOnUnload: false,
styleManager: {
custom: true,
},
});
const sm = editor.StyleManager;
// Make the editor global in the app
Vue.mixin({ data() { return { editor } } });
// Property field component
Vue.component('property-field', {
props: { prop: Object },
template: '#property-field',
computed: {
labelCls() {
const { prop } = this;
const parent = prop.getParent();
const hasParentValue = prop.hasValueParent() && (parent ? parent.isDetached() : true);
return ['flex-nowrap', prop.canClear() && 'indigo--text text--accent-1', hasParentValue && 'orange--text'];
},
inputValue() {
return this.prop.hasValue() ? this.prop.getValue() : '';
},
propName() {
return this.prop.getName();
},
propType() {
return this.prop.getType();
},
defValue() {
return this.prop.getDefaultValue();
},
toOptions() {
const { prop } = this;
return prop.getOptions().map(o => ({ value: prop.getOptionId(o), text: prop.getOptionLabel(o) }))
},
},
methods: {
handleChange(value) {
this.prop.upValue(value);
},
handleInput(value) {
this.prop.upValue(value, { partial: true });
},
openAssets(prop) {
const { Assets } = this.editor;
Assets.open({
select: (asset, complete) => {
prop.upValue(asset.getSrc(), { partial: !complete });
complete && Assets.close();
},
types: ['image'],
accept: 'image/*',
})
}
}
});
const app = new Vue({
vuetify: new Vuetify({
theme: { dark: true },
}),
el: '.style-manager',
data: { sectors: [] },
mounted() {
editor.on('style:custom', this.handleCustom);
},
destroyed() {
editor.off('style:custom', this.handleCustom);
},
methods: {
handleCustom(props = {}) {
const { container } = props;
if...