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,
   ...