GrapesJS Custom Layer 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>
<div id="gjs">
<div style="padding: 25px">Custom Layer Manager</div>
<div>Element A</div>
<div style="padding: 20px">
<div>Element B1</div>
<div>Element B2</div>
<div>Element B3</div>
</div>
<div>Element C</div>
</div>
<div style="display: none">
<div
class="layer-manager"
@pointerdown="onDragStart"
@pointermove="onDragMove"
@pointerup="onDragEnd"
>
<layer-item v-if="root" :component="root" :level="0"></layer-item>
<div
v-if="dragIndicator.show"
class="layer-drag-indicator"
:style="{ top: `${dragIndicator.y}px`, marginLeft: `${dragIndicator.offset}px`, width: `calc(100% - ${dragIndicator.offset}px)` }"></div>
</div>
<div id="layer-item-template" style="display: none;">
<div :class="['layer-item', !visible && 'hidden']">
<div
:class="['layer-item-row', selected && 'selected', hovered && 'hovered']"
@click="setSelected"
@mouseenter="setHover(true)"
@mouseleave="setHover(false)"
ref="layerRef"
data-layer-item
>
<div class="layer-item-icon layer-item-eye" @click.stop="toggleVisibility()">
<svg v-if="visible" viewBox="0 0 24 24"><path fill="currentColor" d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z" /></svg>
<svg v-else viewBox="0 0 24 24"><path fill="currentColor" d="M11.83,9L15,12.16C15,12.11 15,12.05 15,12A3,3 0 0,0 12,9C11.94,9 11.89,9 11.83,9M7.53,9.8L9.08,11.35C9.03,11.56 9,11.77 9,12A3,3 0 0,0 12,15C12.22,15 12.44,14.97 12.65,14.92L14.2,16.47C13.53,16.8 12.79,17 12,17A5,5 0 0,1 7,12C7,11.21 7.2,10.47...
CSS
body, html {
margin: 0;
height: 100%;
}
.layer-manager {
position: relative;
text-align: left;
}
.layer-item.hidden {
opacity: 0.5;
}
.layer-item-icon {
width: 15px;
cursor: pointer;
}
.layer-item-eye {
}
.layer-item-chevron {
transform: rotate(90deg);
}
.layer-item-chevron.open {
transform: rotate(180deg);
}
.layer-item-chevron.hidden {
opacity: 0;
pointer-events: none;
}
.layer-item-row {
display: flex;
align-items: center;
user-select: none;
gap: 8px;
padding: 5px 8px;
border-bottom: 1px solid rgba(0,0,0,0.35);
}
.layer-item-row.selected {
background-color: rgba(255,255,255,0.15);
}
.layer-item-row.hovered {
background-color: rgba(255,255,255,0.05);
}
.layer-item-name {
margin-left: 3px;
}
.layer-item-name.editing {
background-color: white;
color: #555;
padding: 0 3px;
}
.layer-item-name-cnt {
display: flex;
align-items: center;
flex-grow: 1;
}
.layer-drag-indicator {
position: absolute;
width: 100%;
height: 1px;
left: 0;
background-color: #3b97e3;
}
Babel + JSX
const editor = grapesjs.init({
container: '#gjs',
fromElement: true,
height: '100%',
storageManager: false,
noticeOnUnload: false,
layerManager: { custom: true },
});
const { Components, Layers } = editor;
const cmpElMap = new WeakMap();
// Layer item component
Vue.component('layer-item', {
template: '#layer-item-template',
props: { component: Object, level: Number },
data() {
return {
name: '',
components: [],
visible: true,
open: false,
selected: false,
hovered: false,
editing: false,
}
},
mounted() {
this.updateLayer(Layers.getLayerData(this.component));
cmpElMap.set(this.$refs.layerRef, this.component);
editor.on('layer:component', this.onLayerComponentUpdate);
},
destroyed() {
editor.off('layer:component', this.onLayerComponentUpdate);
},
methods: {
onLayerComponentUpdate(cmp) {
if (cmp === this.component) {
this.updateLayer(Layers.getLayerData(cmp));
}
},
updateLayer(data) {
this.name = data.name;
this.components = data.components;
this.visible = data.visible;
this.open = data.open;
this.selected = data.selected;
this.hovered = data.hovered;
},
toggleVisibility() {
const { component } = this;
Layers.setVisible(this.component, !this.visible);
},
toggleOpen() {
const { component } = this;
Layers.setOpen(this.component, !this.open);
},
setHover(hovered) {
Layers.setLayerData(this.component, { hovered })
},
setSelected(event) {
Layers.setLayerData(this.component, { selected: true }, { event })
},
setEditing(value) {
this.editing = value;
const el = this.$refs.nameInput;
if (!value) {
Layers.setName(this.component, el.innerText)
} else {
setTimeout(() => el.focus())
}
},
}
});
// Load the Vue app
const app = new Vue({
el: '.layer-manager',
data: {
root: null,
...