<template> <div :key="element.id" class="element-preview" :class="{ 'element-preview--active': isSelected, 'element-preview--parent-of-selected': isParentOfSelectedElement, 'element-preview--in-error': inError, 'element-preview--first-element': isFirstElement, }" tabindex="0" @click="onSelect" @keyup.d.stop="duplicateElement" @keyup.delete.stop="deleteElement" @keyup.p.stop="selectParentElement" > <InsertElementButton v-show="isSelected" class="element-preview__insert element-preview__insert--top" @click="showAddElementModal(PLACEMENTS.BEFORE)" /> <ElementMenu v-if="isSelected" :placements="placements" :placements-disabled="placementsDisabled" :is-duplicating="isDuplicating" :has-parent="!!parentElement" @delete="deleteElement" @move="$emit('move', $event)" @duplicate="duplicateElement" @select-parent="actionSelectElement({ element: parentElement })" /> <PageElement :element="element" :mode="mode" class="element--read-only" /> <InsertElementButton v-show="isSelected" class="element-preview__insert element-preview__insert--bottom" @click="showAddElementModal(PLACEMENTS.AFTER)" /> <AddElementModal ref="addElementModal" :element-types-allowed="elementTypesAllowed" :page="page" /> <i v-if="inError" class="element-preview__error-icon iconoir-warning-circle" ></i> </div> </template> <script> import ElementMenu from '@baserow/modules/builder/components/elements/ElementMenu' import InsertElementButton from '@baserow/modules/builder/components/elements/InsertElementButton' import PageElement from '@baserow/modules/builder/components/page/PageElement' import { PLACEMENTS } from '@baserow/modules/builder/enums' import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal' import { notifyIf } from '@baserow/modules/core/utils/error' import { mapActions, mapGetters } from 'vuex' import { checkIntermediateElements } from '@baserow/modules/core/utils/dom' export default { name: 'ElementPreview', components: { AddElementModal, ElementMenu, InsertElementButton, PageElement, }, inject: ['builder', 'page', 'mode'], props: { element: { type: Object, required: true, }, isLastElement: { type: Boolean, required: false, default: false, }, isFirstElement: { type: Boolean, required: false, default: false, }, placements: { type: Array, required: false, default: () => [PLACEMENTS.BEFORE, PLACEMENTS.AFTER], }, placementsDisabled: { type: Array, required: false, default: () => [], }, isRootElement: { type: Boolean, required: false, default: false, }, }, data() { return { isDuplicating: false, } }, watch: { /** * Focuses the element if the element has been selected. */ isSelected(newValue, old) { if (newValue && !old) { this.$el.focus() } }, }, computed: { ...mapGetters({ elementSelected: 'element/getSelected', elementAncestors: 'element/getAncestors', }), PLACEMENTS: () => PLACEMENTS, elementTypesAllowed() { return this.parentElementType?.childElementTypes || null }, isSelected() { return this.element.id === this.elementSelected?.id }, selectedElementAncestorIds() { if (!this.elementSelected) { return [] } return this.elementAncestors(this.page, this.elementSelected).map( ({ id }) => id ) }, isParentOfSelectedElement() { return this.selectedElementAncestorIds.includes(this.element.id) }, elementType() { return this.$registry.get('element', this.element.type) }, parentElement() { if (!this.element.parent_element_id) { return null } return this.$store.getters['element/getElementById']( this.page, this.element.parent_element_id ) }, parentElementType() { return this.parentElement ? this.$registry.get('element', this.parentElement?.type) : null }, siblingElements() { return this.$store.getters['element/getSiblings'](this.page, this.element) }, samePlaceInContainerElements() { return this.siblingElements.filter( (e) => e.place_in_container === this.element.place_in_container ) }, nextElement() { return [...this.samePlaceInContainerElements].find((e) => e.order.gt(this.element.order) ) }, inError() { return this.elementType.isInError({ element: this.element, builder: this.builder, }) }, }, methods: { ...mapActions({ actionDuplicateElement: 'element/duplicate', actionDeleteElement: 'element/delete', actionSelectElement: 'element/select', }), onSelect($event) { // Here we check that the event has been emitted for this particular element // If we found an intermediate DOM element with the class `element-preview` // It means it hasn't been originated by this element so we don't select it. if ( !checkIntermediateElements(this.$el, $event.target, (el) => { return el.classList.contains('element-preview') }) ) { this.actionSelectElement({ element: this.element }) } }, showAddElementModal(placement) { this.$refs.addElementModal.show({ placeInContainer: this.element.place_in_container, parentElementId: this.element.parent_element_id, beforeId: this.getBeforeId(placement), }) }, getBeforeId(placement) { return placement === PLACEMENTS.BEFORE ? this.element.id : this.nextElement?.id || null }, async duplicateElement() { this.isDuplicating = true try { await this.actionDuplicateElement({ page: this.page, elementId: this.element.id, }) } catch (error) { notifyIf(error) } this.isDuplicating = false }, async deleteElement() { try { await this.actionDeleteElement({ page: this.page, elementId: this.element.id, }) } catch (error) { notifyIf(error) } }, selectParentElement() { if (this.parentElement) { this.actionSelectElement({ element: this.parentElement }) } }, }, } </script>