<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, 'element-preview--not-visible': !isVisible && !isSelected && !isParentOfSelectedElement, }" @click="onSelect" > <div v-if="isSelected" class="element-preview__name"> {{ elementType.name }} <i v-if="!isVisible" class="iconoir-eye-off" /> </div> <InsertElementButton v-show="isSelected" v-if="canCreate" class="element-preview__insert element-preview__insert--top" @click="showAddElementModal(DIRECTIONS.BEFORE)" /> <ElementMenu v-if="isSelected && canUpdate" :directions="directions" :allowed-directions="allowedMoveDirections" :is-duplicating="isDuplicating" :has-parent="!!parentElement" @delete="deleteElement" @move="onMove" @duplicate="duplicateElement" @select-parent="selectParentElement()" /> <PageElement :element="element" :mode="mode" class="element--read-only" :application-context-additions="applicationContextAdditions" v-on="$listeners" /> <InsertElementButton v-show="isSelected" v-if="canCreate" class="element-preview__insert element-preview__insert--bottom" @click="showAddElementModal(DIRECTIONS.AFTER)" /> <AddElementModal v-if="canCreate" ref="addElementModal" :page="elementPage" /> <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 { DIRECTIONS } 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' import { VISIBILITY_NOT_LOGGED, VISIBILITY_LOGGED_IN, ROLE_TYPE_ALLOW_EXCEPT, ROLE_TYPE_DISALLOW_EXCEPT, } from '@baserow/modules/builder/constants' export default { name: 'ElementPreview', components: { AddElementModal, ElementMenu, InsertElementButton, PageElement, }, inject: ['workspace', 'builder', 'mode', 'currentPage'], props: { element: { type: Object, required: true, }, isFirstElement: { type: Boolean, required: false, default: false, }, applicationContextAdditions: { type: Object, required: false, default: null, }, }, data() { return { isDuplicating: false, } }, computed: { ...mapGetters({ elementSelected: 'element/getSelected', elementAncestors: 'element/getAncestors', getClosestSiblingElement: 'element/getClosestSiblingElement', loggedUser: 'userSourceUser/getUser', }), elementPage() { // We use the page from the element itself return this.$store.getters['page/getById']( this.builder, this.element.page_id ) }, isVisible() { if ( !this.elementType.isVisible({ element: this.element, currentPage: this.currentPage, }) ) { return false } const isAuthenticated = this.$store.getters[ 'userSourceUser/isAuthenticated' ](this.builder) const user = this.loggedUser(this.builder) const roles = this.element.roles const roleType = this.element.role_type const visibility = this.element.visibility if (visibility === VISIBILITY_LOGGED_IN) { if (!isAuthenticated) { return false } if (roleType === ROLE_TYPE_ALLOW_EXCEPT) { return !roles.includes(user.role) } else if (roleType === ROLE_TYPE_DISALLOW_EXCEPT) { return roles.includes(user.role) } else { return true } } else if (visibility === VISIBILITY_NOT_LOGGED) { return !isAuthenticated } else { return true } }, DIRECTIONS: () => DIRECTIONS, directions() { return [ DIRECTIONS.BEFORE, DIRECTIONS.AFTER, DIRECTIONS.LEFT, DIRECTIONS.RIGHT, ] }, parentOfElementSelected() { if (!this.elementSelected?.parent_element_id) { return null } return this.$store.getters['element/getElementById']( this.elementPage, this.elementSelected.parent_element_id ) }, elementsAround() { return this.elementType.getElementsAround({ builder: this.builder, page: this.currentPage, withSharedPage: true, element: this.element, }) }, nextPlaces() { return this.elementType.getNextPlaces({ builder: this.builder, page: this.elementPage, element: this.element, }) }, allowedMoveDirections() { return Object.entries(this.nextPlaces) .filter(([, nextPlace]) => !!nextPlace) .map(([direction]) => direction) }, canCreate() { return this.$hasPermission( 'builder.page.create_element', this.currentPage, this.workspace.id ) }, canUpdate() { return this.$hasPermission( 'builder.page.element.update', this.element, this.workspace.id ) }, isSelected() { return this.element.id === this.elementSelected?.id }, selectedElementAncestorIds() { if (!this.elementSelected) { return [] } return this.elementAncestors(this.elementPage, 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.elementPage, this.element.parent_element_id ) }, inError() { return this.elementType.isInError({ page: this.elementPage, element: this.element, builder: this.builder, }) }, }, watch: { /** * If the element is currently selected, i.e. in the Elements Context menu, * ensure the element is scrolled into the viewport. */ isSelected(newValue, old) { if (newValue && !old) { const rect = this.$el.getBoundingClientRect() const isTopVisible = rect.top >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight) if (!isTopVisible) { this.$el.scrollIntoView({ behavior: 'smooth', block: 'start' }) } } }, /** * If the currently selected element in the Page Preview has moved, ensure * the element is scrolled into the viewport. */ element: { handler(newValue, old) { if ( (newValue.place_in_container !== old.place_in_container || newValue.order !== old.order) && this.isSelected ) { this.$el.scrollIntoView({ behavior: 'smooth', block: 'center' }) } }, deep: true, }, }, mounted() { if (this.isFirstElement) { this.actionSelectElement({ element: this.element }) } }, methods: { ...mapActions({ actionDuplicateElement: 'element/duplicate', actionDeleteElement: 'element/delete', actionSelectElement: 'element/select', }), onMove(direction) { this.$emit('move', { element: this.element, direction }) }, 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`, // or `element-preview__menu`, then we don't select the element. // 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') || el.classList.contains('element-preview__menu') ) }) ) { this.actionSelectElement({ element: this.element }) } }, showAddElementModal(direction) { const rootElement = this.$store.getters['element/getAncestors']( this.elementPage, this.element, { includeSelf: true } )[0] const rootElementType = this.$registry.get('element', rootElement.type) const pagePlace = rootElementType.getPagePlace() this.$refs.addElementModal.show({ placeInContainer: this.element.place_in_container, parentElementId: this.element.parent_element_id, beforeId: this.getBeforeId(direction), pagePlace, }) }, getBeforeId(direction) { return direction === DIRECTIONS.BEFORE ? this.element.id : this.elementsAround[DIRECTIONS.AFTER]?.id || null }, async duplicateElement() { this.isDuplicating = true try { await this.actionDuplicateElement({ page: this.elementPage, elementId: this.element.id, }) } catch (error) { notifyIf(error) } this.isDuplicating = false }, async deleteElement() { try { const siblingElementToSelect = this.elementsAround[DIRECTIONS.AFTER] || this.elementsAround[DIRECTIONS.BEFORE] || this.elementsAround[DIRECTIONS.LEFT] || this.elementsAround[DIRECTIONS.RIGHT] || this.parentOfElementSelected await this.actionDeleteElement({ page: this.elementPage, elementId: this.element.id, }) if (siblingElementToSelect?.id) { await this.actionSelectElement({ element: siblingElementToSelect }) } } catch (error) { notifyIf(error) } }, selectParentElement() { if (this.parentOfElementSelected) { this.actionSelectElement({ element: this.parentOfElementSelected }) } }, }, } </script>