1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 22:35:36 +00:00

Resolve "Add Element styles tab"

This commit is contained in:
Bram Wiepjes 2023-06-27 09:29:02 +00:00
parent 4d2f1936b1
commit a1e84d703d
41 changed files with 646 additions and 489 deletions

View file

@ -49,7 +49,7 @@ class PublicElementSerializer(serializers.ModelSerializer):
class Meta:
model = Element
fields = ("id", "type")
fields = ("id", "type", "style_padding_top", "style_padding_bottom")
extra_kwargs = {
"id": {"read_only": True},
"type": {"read_only": True},

View file

@ -22,7 +22,14 @@ class ElementSerializer(serializers.ModelSerializer):
class Meta:
model = Element
fields = ("id", "page_id", "type", "order")
fields = (
"id",
"page_id",
"type",
"order",
"style_padding_top",
"style_padding_bottom",
)
extra_kwargs = {
"id": {"read_only": True},
"page_id": {"read_only": True},
@ -50,13 +57,13 @@ class CreateElementSerializer(serializers.ModelSerializer):
class Meta:
model = Element
fields = ("before_id", "type")
fields = ("before_id", "type", "style_padding_top", "style_padding_bottom")
class UpdateElementSerializer(serializers.ModelSerializer):
class Meta:
model = Element
fields = []
fields = ("style_padding_top", "style_padding_bottom")
class MoveElementSerializer(serializers.Serializer):

View file

@ -1,4 +1,3 @@
from abc import ABC
from typing import Dict, Optional
from rest_framework import serializers
@ -21,17 +20,70 @@ from baserow.contrib.builder.types import ElementDict
from baserow.core.user_files.models import UserFile
class BaseTextElementType(ElementType, ABC):
class HeadingElementType(ElementType):
"""
Base class for text elements.
A simple heading element that can be used to display a title.
"""
type = "heading"
model_class = HeadingElement
serializer_field_names = ["value", "level"]
allowed_fields = ["value", "level"]
class SerializedDict(ElementDict):
value: Expression
level: int
@property
def serializer_field_overrides(self):
from baserow.core.expression.serializers import ExpressionSerializer
overrides = {
"value": ExpressionSerializer(
help_text="The value of the element. Must be an expression.",
required=False,
allow_blank=True,
default="",
),
"level": serializers.IntegerField(
help_text="The level of the heading from 1 to 6.",
min_value=1,
max_value=6,
default=1,
),
}
return overrides
def get_sample_params(self):
return {
"value": "Corporis perspiciatis",
"level": 2,
}
class ParagraphElementType(ElementType):
"""
A simple paragraph element that can be used to display a paragraph of text.
"""
type = "paragraph"
model_class = ParagraphElement
serializer_field_names = ["value"]
allowed_fields = ["value"]
class SerializedDict(ElementDict):
value: Expression
def get_sample_params(self):
return {
"value": "Suscipit maxime eos ea vel commodi dolore. "
"Eum dicta sit rerum animi. Sint sapiente eum cupiditate nobis vel. "
"Maxime qui nam consequatur. "
"Asperiores corporis perspiciatis nam harum veritatis. "
"Impedit qui maxime aut illo quod ea molestias."
}
@property
def serializer_field_overrides(self):
from baserow.core.expression.serializers import ExpressionSerializer
@ -46,65 +98,7 @@ class BaseTextElementType(ElementType, ABC):
}
class HeadingElementType(BaseTextElementType):
"""
A simple heading element that can be used to display a title.
"""
type = "heading"
model_class = HeadingElement
class SerializedDict(ElementDict):
value: Expression
level: int
@property
def serializer_field_names(self):
return super().serializer_field_names + ["level"]
@property
def allowed_fields(self):
return super().allowed_fields + ["level"]
@property
def serializer_field_overrides(self):
overrides = {
"level": serializers.IntegerField(
help_text="The level of the heading from 1 to 6.",
min_value=1,
max_value=6,
default=1,
)
}
overrides.update(super().serializer_field_overrides)
return overrides
def get_sample_params(self):
return {
"value": "Corporis perspiciatis",
"level": 2,
}
class ParagraphElementType(BaseTextElementType):
"""
A simple paragraph element that can be used to display a paragraph of text.
"""
type = "paragraph"
model_class = ParagraphElement
def get_sample_params(self):
return {
"value": "Suscipit maxime eos ea vel commodi dolore. "
"Eum dicta sit rerum animi. Sint sapiente eum cupiditate nobis vel. "
"Maxime qui nam consequatur. "
"Asperiores corporis perspiciatis nam harum veritatis. "
"Impedit qui maxime aut illo quod ea molestias."
}
class LinkElementType(BaseTextElementType):
class LinkElementType(ElementType):
"""
A simple paragraph element that can be used to display a paragraph of text.
"""
@ -112,38 +106,34 @@ class LinkElementType(BaseTextElementType):
type = "link"
model_class = LinkElement
PATH_PARAM_TYPE_TO_PYTHON_TYPE_MAP = {"text": str, "numeric": int}
serializer_field_names = [
"value",
"navigation_type",
"navigate_to_page_id",
"navigate_to_url",
"page_parameters",
"variant",
"target",
"width",
"alignment",
]
allowed_fields = [
"value",
"navigation_type",
"navigate_to_page_id",
"navigate_to_url",
"page_parameters",
"variant",
"target",
"width",
"alignment",
]
class SerializedDict(ElementDict):
value: Expression
destination: Expression
open_new_tab: bool
@property
def serializer_field_names(self):
return super().serializer_field_names + [
"navigation_type",
"navigate_to_page_id",
"navigate_to_url",
"page_parameters",
"variant",
"target",
"width",
"alignment",
]
@property
def allowed_fields(self):
return super().allowed_fields + [
"navigation_type",
"navigate_to_page_id",
"navigate_to_url",
"page_parameters",
"variant",
"target",
"width",
"alignment",
]
def import_serialized(self, page, serialized_values, id_mapping):
serialized_copy = serialized_values.copy()
if serialized_copy["navigate_to_page_id"]:
@ -160,6 +150,12 @@ class LinkElementType(BaseTextElementType):
from baserow.core.expression.serializers import ExpressionSerializer
overrides = {
"value": ExpressionSerializer(
help_text="The value of the element. Must be an expression.",
required=False,
allow_blank=True,
default="",
),
"navigation_type": serializers.ChoiceField(
choices=LinkElement.NAVIGATION_TYPES.choices,
help_text=LinkElement._meta.get_field("navigation_type").help_text,
@ -203,11 +199,11 @@ class LinkElementType(BaseTextElementType):
required=False,
),
}
overrides.update(super().serializer_field_overrides)
return overrides
def get_sample_params(self):
return {
"value": "test",
"navigation_type": "custom",
"navigate_to_page_id": None,
"navigate_to_url": "http://example.com",

View file

@ -110,7 +110,7 @@ class ElementHandler:
else:
order = Element.get_last_order(page)
shared_allowed_fields = ["type"]
shared_allowed_fields = ["type", "style_padding_top", "style_padding_bottom"]
allowed_values = extract_allowed(
kwargs, shared_allowed_fields + element_type.allowed_fields
)
@ -145,7 +145,7 @@ class ElementHandler:
element_type = element_type_registry.get_by_model(element)
shared_allowed_fields = []
shared_allowed_fields = ["style_padding_top", "style_padding_bottom"]
allowed_updates = extract_allowed(
kwargs, shared_allowed_fields + element_type.allowed_fields
)

View file

@ -51,6 +51,9 @@ class Element(
on_delete=models.SET(get_default_element_content_type),
)
style_padding_top = models.PositiveIntegerField(default=10)
style_padding_bottom = models.PositiveIntegerField(default=10)
class Meta:
ordering = ("order", "id")
@ -86,18 +89,7 @@ class Element(
return cls.get_unique_orders_before_item(before, queryset)[0]
class BaseTextElement(Element):
"""
Base class for text elements.
"""
value = ExpressionField(default="")
class Meta:
abstract = True
class HeadingElement(BaseTextElement):
class HeadingElement(Element):
"""
A Heading element to display a title.
"""
@ -109,18 +101,21 @@ class HeadingElement(BaseTextElement):
H4 = 4
H5 = 5
value = ExpressionField(default="")
level = models.IntegerField(
choices=HeadingLevel.choices, default=1, help_text="The level of the heading"
)
class ParagraphElement(BaseTextElement):
class ParagraphElement(Element):
"""
A simple paragraph.
"""
value = ExpressionField(default="")
class LinkElement(BaseTextElement):
class LinkElement(Element):
"""
A simple link.
"""
@ -141,6 +136,7 @@ class LinkElement(BaseTextElement):
AUTO = "auto"
FULL = "full"
value = ExpressionField(default="")
navigation_type = models.CharField(
choices=NAVIGATION_TYPES.choices,
help_text="The navigation type.",

View file

@ -58,7 +58,12 @@ class ElementType(
other_properties = {key: getattr(element, key) for key in self.allowed_fields}
serialized = self.SerializedDict(
id=element.id, type=self.type, order=element.order, **other_properties
id=element.id,
type=self.type,
order=element.order,
style_padding_top=element.style_padding_top,
style_padding_bottom=element.style_padding_bottom,
**other_properties
)
return serialized

View file

@ -0,0 +1,22 @@
# Generated by Django 3.2.18 on 2023-06-07 20:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("builder", "0013_datasource"),
]
operations = [
migrations.AddField(
model_name="element",
name="style_padding_bottom",
field=models.PositiveIntegerField(default=10),
),
migrations.AddField(
model_name="element",
name="style_padding_top",
field=models.PositiveIntegerField(default=10),
),
]

View file

@ -49,12 +49,16 @@ def test_builder_application_export(data_fixture):
"id": element1.id,
"type": "heading",
"order": element1.order,
"style_padding_top": 10,
"style_padding_bottom": 10,
"value": element1.value,
"level": element1.level,
},
{
"id": element2.id,
"type": "paragraph",
"style_padding_top": 10,
"style_padding_bottom": 10,
"order": element2.order,
"value": element2.value,
},
@ -70,6 +74,8 @@ def test_builder_application_export(data_fixture):
{
"id": element3.id,
"type": "heading",
"style_padding_top": 10,
"style_padding_bottom": 10,
"order": element3.order,
"value": element3.value,
"level": element3.level,

View file

@ -1,38 +1,44 @@
<template>
<div class="element__menu">
<div class="element-preview__menu">
<div
v-if="isDuplicating"
class="loading element__menu-duplicate-loading"
class="loading element-preview__menu-duplicate-loading"
></div>
<a v-else class="element__menu-item" @click="$emit('duplicate')">
<a v-else class="element-preview__menu-item" @click="$emit('duplicate')">
<i class="fas fa-copy"></i>
<span class="element__menu-item-description">
<span class="element-preview__menu-item-description">
{{ $t('action.duplicate') }}
</span>
</a>
<a
class="element__menu-item"
class="element-preview__menu-item"
:class="{ disabled: moveUpDisabled }"
@click="!moveUpDisabled && $emit('move', PLACEMENTS.BEFORE)"
>
<i class="fas fa-arrow-up"></i>
<span v-if="!moveUpDisabled" class="element__menu-item-description">
<span
v-if="!moveUpDisabled"
class="element-preview__menu-item-description"
>
{{ $t('elementMenu.moveUp') }}
</span>
</a>
<a
class="element__menu-item"
class="element-preview__menu-item"
:class="{ disabled: moveDownDisabled }"
@click="!moveDownDisabled && $emit('move', PLACEMENTS.AFTER)"
>
<i class="fas fa-arrow-down"></i>
<span v-if="!moveDownDisabled" class="element__menu-item-description">
<span
v-if="!moveDownDisabled"
class="element-preview__menu-item-description"
>
{{ $t('elementMenu.moveDown') }}
</span>
</a>
<a class="element__menu-item" @click="$emit('delete')">
<a class="element-preview__menu-item" @click="$emit('delete')">
<i class="fas fa-trash"></i>
<span class="element__menu-item-description">
<span class="element-preview__menu-item-description">
{{ $t('action.delete') }}
</span>
</a>

View file

@ -1,12 +1,15 @@
<template>
<div
class="element"
:class="{ 'element--active': active, 'element--in-error': inError }"
class="element-preview"
:class="{
'element-preview--active': active,
'element-preview--in-error': inError,
}"
@click="$emit('selected')"
>
<InsertElementButton
v-if="active"
class="element__insert--top"
class="element-preview__insert--top"
@click="$emit('insert', PLACEMENTS.BEFORE)"
/>
<ElementMenu
@ -18,15 +21,14 @@
@move="$emit('move', $event)"
@duplicate="$emit('duplicate')"
/>
<component
:is="elementType.editComponent"
class="element__component"
<PageRootElement
:element="element"
:builder="builder"
/>
:mode="'editing'"
></PageRootElement>
<InsertElementButton
v-if="active"
class="element__insert--bottom"
class="element-preview__insert--bottom"
@click="$emit('insert', PLACEMENTS.AFTER)"
/>
</div>
@ -35,10 +37,12 @@
<script>
import ElementMenu from '@baserow/modules/builder/components/elements/ElementMenu'
import InsertElementButton from '@baserow/modules/builder/components/elements/InsertElementButton'
import PageRootElement from '@baserow/modules/builder/components/page/PageRootElement'
import { PLACEMENTS } from '@baserow/modules/builder/enums'
export default {
name: 'ElementPreview',
components: { ElementMenu, InsertElementButton },
components: { ElementMenu, InsertElementButton, PageRootElement },
inject: ['builder'],
props: {
element: {

View file

@ -1,5 +1,5 @@
<template>
<a class="element__insert" @click="$emit('click')">
<a class="element-preview__insert" @click="$emit('click')">
<i class="fas fa-plus"></i>
</a>
</template>

View file

@ -9,11 +9,11 @@
</template>
<script>
import textElement from '@baserow/modules/builder/mixins/elements/textElement'
import element from '@baserow/modules/builder/mixins/element'
export default {
name: 'HeadingElement',
mixins: [textElement],
mixins: [element],
props: {
/**
* @type {Object}

View file

@ -9,10 +9,12 @@
</template>
<script>
import element from '@baserow/modules/builder/mixins/element'
import { IMAGE_SOURCE_TYPES } from '@baserow/modules/builder/enums'
export default {
name: 'ImageElement',
mixins: [element],
props: {
/**
* @type {Object}
@ -36,6 +38,7 @@ export default {
classes() {
return {
[`element--alignment-${this.element.alignment}`]: true,
'element--no-value': !this.imageSource && !this.element.alt_text,
}
},
},

View file

@ -1,21 +1,15 @@
<template>
<div class="link-element" :class="classes">
<Button
v-if="element.variant === 'button'"
tag="a"
v-bind="extraAttr"
:target="element.target"
:full-width="element.width === 'full'"
@click="onClick($event)"
>
{{ element.value || $t('linkElement.noValue') }}
</Button>
<a
v-else
class="link-element__link"
:class="{
'link-element__link': element.variant !== 'button',
'link-element__button': element.variant === 'button',
'link-element__button--full-width':
element.variant === 'button' && element.width === 'full',
}"
v-bind="extraAttr"
:target="`_${element.target}`"
@click="onClick($event)"
@click="onClick"
>
{{ element.value || $t('linkElement.noValue') }}
</a>
@ -23,7 +17,7 @@
</template>
<script>
import textElement from '@baserow/modules/builder/mixins/elements/textElement'
import element from '@baserow/modules/builder/mixins/element'
import { LinkElementType } from '@baserow/modules/builder/elementTypes'
/**
@ -40,18 +34,7 @@ import { LinkElementType } from '@baserow/modules/builder/elementTypes'
export default {
name: 'LinkElement',
mixins: [textElement],
props: {
/**
* @type {LinkElement}
*/
element: {
type: Object,
required: true,
},
builder: { type: Object, required: true },
mode: { type: String, required: true },
},
mixins: [element],
computed: {
classes() {
return {
@ -100,6 +83,11 @@ export default {
},
methods: {
onClick(event) {
if (this.mode === 'editing') {
event.preventDefault()
return
}
if (!this.url) {
event.preventDefault()
} else if (

View file

@ -1,65 +0,0 @@
<template>
<div class="link-element" :class="classes">
<Button
v-if="element.variant === 'button'"
tag="a"
v-bind="extraAttr"
:target="element.target"
:full-width="element.width === 'full'"
@click.prevent
>
{{ element.value || $t('linkElement.noValue') }}
</Button>
<a
v-else
class="link-element__link"
v-bind="extraAttr"
:target="`_${element.target}`"
@click.prevent
>
{{ element.value || $t('linkElement.noValue') }}
</a>
</div>
</template>
<script>
import textElement from '@baserow/modules/builder/mixins/elements/textElement'
import { LinkElementType } from '@baserow/modules/builder/elementTypes'
export default {
name: 'LinkElementEdit',
mixins: [textElement],
props: {
/**
* @type {LinkElement}
*/
element: {
type: Object,
required: true,
},
builder: { type: Object, required: true },
},
computed: {
classes() {
return {
[`element--alignment-${this.element.alignment}`]: true,
'element--no-value': !this.element.value,
}
},
extraAttr() {
const attr = {}
if (this.url) {
attr.href = this.url
}
return attr
},
url() {
try {
return LinkElementType.getUrlFromElement(this.element, this.builder)
} catch (e) {
return ''
}
},
},
}
</script>

View file

@ -14,7 +14,7 @@
</template>
<script>
import textElement from '@baserow/modules/builder/mixins/elements/textElement'
import element from '@baserow/modules/builder/mixins/element'
import { generateHash } from '@baserow/modules/core/utils/hashing'
/**
@ -25,7 +25,7 @@ import { generateHash } from '@baserow/modules/core/utils/hashing'
export default {
name: 'ParagraphElement',
mixins: [textElement],
mixins: [element],
props: {
/**
* @type {Object}

View file

@ -1,11 +1,9 @@
<template>
<div>
<component
:is="getType(element).component"
<PageRootElement
v-for="element in elements"
:key="element.id"
:element="element"
class="element__component"
:builder="builder"
:mode="mode"
/>
@ -13,7 +11,10 @@
</template>
<script>
import PageRootElement from '@baserow/modules/builder/components/page/PageRootElement'
export default {
components: { PageRootElement },
inject: ['builder', 'mode'],
props: {
page: {
@ -33,10 +34,5 @@ export default {
required: true,
},
},
methods: {
getType(element) {
return this.$registry.get('element', element.type)
},
},
}
</script>

View file

@ -0,0 +1,53 @@
<template functional>
<!--
This element is supposed to be wrapping the root elements on a page. They allow
setting a width, background, borders, and more, but this only makes sense if they're
added to the root of the page. Child elements in for example a containing element must
not be wrapped by this component.
-->
<div
class="page-root-element"
:style="{
'padding-top': `${props.element.style_padding_top || 0}px`,
'padding-bottom': `${props.element.style_padding_bottom || 0}px`,
}"
>
<div class="page-root-element__inner">
<component
:is="$options.methods.getComponent(parent, props.element, props.mode)"
:element="props.element"
:builder="props.builder"
:mode="props.mode"
class="element"
/>
</div>
</div>
</template>
<script>
export default {
name: 'PageRootElement',
props: {
element: {
type: Object,
required: true,
},
builder: {
type: Object,
required: true,
},
mode: {
type: String,
required: false,
default: '',
},
},
methods: {
getComponent(parent, element, mode) {
const elementType = parent.$registry.get('element', element.type)
const componentName = mode === 'editing' ? 'editComponent' : 'component'
return elementType[componentName]
},
},
}
</script>

View file

@ -11,50 +11,10 @@
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
import { clone } from '@baserow/modules/core/utils/object'
import _ from 'lodash'
import elementSidePanel from '@baserow/modules/builder/mixins/elementSidePanel'
export default {
name: 'GeneralSidePanel',
inject: ['builder'],
computed: {
...mapGetters({
element: 'element/getSelected',
}),
elementType() {
if (this.element) {
return this.$registry.get('element', this.element.type)
}
return null
},
defaultValues() {
return this.element
},
},
methods: {
...mapActions({
actionDebouncedUpdateSelectedElement: 'element/debouncedUpdateSelected',
}),
async onChange(newValues) {
const oldValues = this.element
if (!_.isEqual(newValues, oldValues)) {
try {
await this.actionDebouncedUpdateSelectedElement({
// Here we clone the values to prevent
// "modification oustide of the store" error
values: clone(newValues),
})
} catch (error) {
// Restore the previous saved values from the store
this.$refs.elementForm.reset()
notifyIf(error)
}
}
},
},
mixins: [elementSidePanel],
}
</script>

View file

@ -0,0 +1,65 @@
<template>
<form @submit.prevent>
<FormElement class="control">
<label class="control__label">{{ label }}</label>
<div class="control__elements">
<input
v-model="values[paddingName]"
type="number"
class="input"
:class="{ 'input--error': $v.values[paddingName].$error }"
@blur="$v.values[paddingName].$touch()"
/>
<div v-if="$v.values[paddingName].$error" class="error">
{{ $t('styleBoxForm.paddingError') }}
</div>
</div>
</FormElement>
</form>
</template>
<script>
import { required, integer, between } from 'vuelidate/lib/validators'
import form from '@baserow/modules/core/mixins/form'
export default {
name: 'StyleBoxForm',
mixins: [form],
props: {
label: {
type: String,
required: true,
},
paddingName: {
type: String,
required: true,
},
},
data() {
return {
allowedValues: [this.paddingName],
values: {
[this.paddingName]: 0,
},
}
},
validations() {
return {
values: {
[this.paddingName]: {
required,
integer,
between: between(0, 200),
},
},
}
},
methods: {
emitChange(newValues) {
if (this.isFormValid()) {
this.$emit('values-changed', newValues)
}
},
},
}
</script>

View file

@ -1,12 +1,27 @@
<template>
<div>Style panel</div>
<div :key="element.id">
<StyleBoxForm
:label="$t('styleSidePanel.paddingTop')"
padding-name="style_padding_top"
:default-values="defaultValues"
@values-changed="onChange($event)"
></StyleBoxForm>
<StyleBoxForm
:label="$t('styleSidePanel.paddingBottom')"
padding-name="style_padding_bottom"
:default-values="defaultValues"
@values-changed="onChange($event)"
></StyleBoxForm>
</div>
</template>
<script>
import elementSidePanel from '@baserow/modules/builder/mixins/elementSidePanel'
import StyleBoxForm from '@baserow/modules/builder/components/page/sidePanels/StyleBoxForm.vue'
export default {
name: 'StyleSidePanel',
data() {
return {}
},
components: { StyleBoxForm },
mixins: [elementSidePanel],
}
</script>

View file

@ -2,7 +2,6 @@ import { Registerable } from '@baserow/modules/core/registry'
import ParagraphElement from '@baserow/modules/builder/components/elements/components/ParagraphElement'
import HeadingElement from '@baserow/modules/builder/components/elements/components/HeadingElement'
import LinkElement from '@baserow/modules/builder/components/elements/components/LinkElement'
import LinkElementEdit from '@baserow/modules/builder/components/elements/components/LinkElementEdit'
import ParagraphElementForm from '@baserow/modules/builder/components/elements/components/forms/ParagraphElementForm'
import HeadingElementForm from '@baserow/modules/builder/components/elements/components/forms/HeadingElementForm'
import LinkElementForm from '@baserow/modules/builder/components/elements/components/forms/LinkElementForm'
@ -131,10 +130,6 @@ export class LinkElementType extends ElementType {
return LinkElement
}
get editComponent() {
return LinkElementEdit
}
get formComponent() {
return LinkElementForm
}

View file

@ -0,0 +1,20 @@
export default {
props: {
element: {
type: Object,
required: true,
},
builder: {
type: Object,
required: true,
},
mode: {
// editing = being editing by the page editor
// preview = previewing the application
// public = publicly published application
type: String,
required: false,
default: '',
},
},
}

View file

@ -0,0 +1,46 @@
import { mapActions, mapGetters } from 'vuex'
import _ from 'lodash'
import { clone } from '@baserow/modules/core/utils/object'
import { notifyIf } from '@baserow/modules/core/utils/error'
export default {
inject: ['builder'],
computed: {
...mapGetters({
element: 'element/getSelected',
}),
elementType() {
if (this.element) {
return this.$registry.get('element', this.element.type)
}
return null
},
defaultValues() {
return this.element
},
},
methods: {
...mapActions({
actionDebouncedUpdateSelectedElement: 'element/debouncedUpdateSelected',
}),
async onChange(newValues) {
const oldValues = this.element
if (!_.isEqual(newValues, oldValues)) {
try {
await this.actionDebouncedUpdateSelectedElement({
// Here we clone the values to prevent
// "modification oustide of the store" error
values: clone(newValues),
})
} catch (error) {
// Restore the previous saved values from the store
this.$refs.elementForm.reset()
notifyIf(error)
}
}
},
},
}

View file

@ -1,9 +0,0 @@
export default {
props: {
value: {
type: String,
required: false,
default: 'Some temp default value',
},
},
}

View file

@ -72,5 +72,14 @@ export default {
mode,
}
},
head() {
return {
titleTemplate: '',
title: this.page.name,
bodyAttrs: {
class: 'public-page',
},
}
},
}
</script>

View file

@ -5,9 +5,11 @@
@import 'add_element_modal';
@import 'page_preview';
@import 'element';
@import 'element_preview';
@import 'side_panels';
@import 'empty_side_panel_state';
@import 'page';
@import 'page_editor';
@import 'page_root_element';
@import 'page_settings_path_params_form_element';
@import 'domain_card';
@import 'dns_status';
@ -15,5 +17,6 @@
@import 'integration_settings';
@import 'last_published_domain_date';
@import 'publish_action_modal';
@import 'public_page';
@import 'data_source_context';
@import 'data_source_form';

View file

@ -1,181 +1,19 @@
.element__menu {
display: flex;
flex-wrap: nowrap;
border: solid 1px $color-neutral-400;
border-radius: 3px;
z-index: 2;
@include absolute(5px, 5px, auto, auto);
}
.element__insert {
@include center-text(26px, 10px);
display: block;
border-radius: 100%;
border: solid 1px $color-neutral-300;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.16);
color: $color-primary-900;
background-color: $white;
&:hover {
background-color: $color-neutral-50;
box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.32);
}
&--top,
&--bottom {
@include absolute(-13px, auto, auto, 50%);
margin-left: -12px;
z-index: 2;
}
&--bottom {
top: auto;
bottom: -12px;
}
}
.element {
position: relative;
.element__insert {
display: none;
}
.element__menu {
display: none;
}
&:hover {
.element__insert {
display: block;
}
.element__menu {
display: flex;
}
}
&:not(.element--active) {
cursor: pointer;
}
&.element--active {
cursor: inherit;
&::before {
@include absolute(0, 0, 0, 0);
content: '';
border: solid 1px $color-primary-500;
pointer-events: none;
}
.element__insert {
display: block;
}
}
}
.element__menu-item-description {
@include absolute(-25px, -2px, auto, auto);
display: none;
background-color: $color-neutral-900;
font-size: 11px;
color: $white;
line-height: 20px;
padding: 0 4px;
border-radius: 3px;
white-space: nowrap;
}
.element__menu-item {
@include center-text(24px, 9px);
position: relative;
background-color: $white;
color: $color-primary-900;
&:hover {
background-color: $color-neutral-100;
.element__menu-item-description {
display: block;
}
}
&:first-child {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
&:last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
&.disabled {
cursor: inherit;
color: $color-neutral-300;
&:hover {
background-color: $white;
}
}
}
.element__component {
margin: 0;
}
.element__menu-duplicate-loading {
margin: 5px;
// this is a placeholder, the class will be added to every element component.
}
.element--no-value {
opacity: 0.3;
}
.element--in-error::after {
@extend .fas;
@include fa-icon;
@include absolute(0, 0, auto, auto);
content: fa-content($fa-var-exclamation-circle);
pointer-events: none;
width: 20px;
height: 20px;
line-height: 20px;
font-size: 20px;
margin-right: 5px;
margin-top: 5px;
color: $color-error-300;
}
.element--alignment-left {
justify-content: start;
.button--full-width {
text-align: left;
}
}
.element--alignment-center {
justify-content: center;
.button--full-width {
text-align: center;
}
}
.element--alignment-right {
justify-content: end;
.button--full-width {
text-align: right;
}
}

View file

@ -0,0 +1,149 @@
.element-preview__menu {
display: flex;
flex-wrap: nowrap;
border: solid 1px $color-neutral-400;
border-radius: 3px;
z-index: 2;
@include absolute(5px, 5px, auto, auto);
}
.element-preview__insert {
@include center-text(26px, 10px);
display: block;
border-radius: 100%;
border: solid 1px $color-neutral-300;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.16);
color: $color-primary-900;
background-color: $white;
&:hover {
background-color: $color-neutral-50;
box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.32);
}
&--top,
&--bottom {
@include absolute(-13px, auto, auto, 50%);
margin-left: -12px;
z-index: 2;
}
&--bottom {
top: auto;
bottom: -12px;
}
}
.element-preview {
position: relative;
.element-preview__insert {
display: none;
}
.element-preview__menu {
display: none;
}
&:hover {
.element-preview__insert {
display: block;
}
.element-preview__menu {
display: flex;
}
}
&:not(.element-preview--active) {
cursor: pointer;
}
&.element-preview--active {
cursor: inherit;
&::before {
@include absolute(0, 0, 0, 0);
content: '';
border: solid 1px $color-primary-500;
pointer-events: none;
}
.element-preview__insert {
display: block;
}
}
}
.element-preview__menu-item-description {
@include absolute(-25px, -2px, auto, auto);
display: none;
background-color: $color-neutral-900;
font-size: 11px;
color: $white;
line-height: 20px;
padding: 0 4px;
border-radius: 3px;
white-space: nowrap;
}
.element-preview__menu-item {
@include center-text(24px, 9px);
position: relative;
background-color: $white;
color: $color-primary-900;
&:hover {
background-color: $color-neutral-100;
.element-preview__menu-item-description {
display: block;
}
}
&:first-child {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
&:last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
&.disabled {
cursor: inherit;
color: $color-neutral-300;
&:hover {
background-color: $white;
}
}
}
.element-preview__menu-duplicate-loading {
margin: 5px;
}
.element-preview--in-error::after {
@extend .fas;
@include fa-icon;
@include absolute(0, 0, auto, auto);
content: fa-content($fa-var-exclamation-circle);
pointer-events: none;
width: 20px;
height: 20px;
line-height: 20px;
font-size: 20px;
margin-right: 5px;
margin-top: 5px;
color: $color-error-300;
}

View file

@ -1,2 +1,2 @@
@import 'paragraphElementForm';
@import 'linkElementForm';
@import 'paragraph_element_form';
@import 'link_element_form';

View file

@ -10,3 +10,11 @@
color: $color-neutral-500;
margin-left: 5px;
}
.link-element-form__params-error {
display: flex;
flex-direction: column;
gap: 10px;
text-align: center;
align-items: center;
}

View file

@ -1,12 +0,0 @@
.paragraph-element-form__value {
resize: vertical;
color: $color-primary-900;
}
.link-element-form__params-error {
display: flex;
flex-direction: column;
gap: 10px;
text-align: center;
align-items: center;
}

View file

@ -0,0 +1,4 @@
.paragraph-element-form__value {
resize: vertical;
color: $color-primary-900;
}

View file

@ -1,29 +1,29 @@
.heading-element {
margin: 0;
color: $black;
}
h1.heading-element {
font-size: 24px;
padding: 32px 82px;
font-size: 30px;
}
h2.heading-element {
font-size: 20px;
padding: 28px 82px;
font-size: 26px;
}
h3.heading-element {
font-size: 18px;
padding: 24px 82px;
font-size: 22px;
}
h4.heading-element {
font-size: 16px;
padding: 20px 82px;
font-size: 18px;
}
h5.heading-element {
font-size: 15px;
padding: 18px 82px;
font-size: 14px;
}
h6.heading-element {
font-size: 14px;
padding: 16px 82px;
font-style: italic;
}

View file

@ -1,13 +1,45 @@
.link-element {
display: flex;
padding: 5px 82px;
}
.link-element__link {
font-size: 14px;
font-weight: 700;
padding: 0;
height: 32px;
line-height: 32px;
border: 1px solid transparent;
color: $black;
text-decoration: underline;
}
.link-element__button {
font-size: 14px;
cursor: pointer;
display: inline-block;
color: $white;
background-color: $black;
line-height: 28px;
padding: 0 12px;
border-radius: 3px;
border: none;
white-space: nowrap;
text-align: left;
text-decoration: none;
&:hover {
background-color: lighten($black, 10%);
text-decoration: none;
}
&:focus {
background-color: lighten($black, 20%);
}
&--full-width {
width: 100%;
}
.element--alignment-center & {
text-align: center;
}
.element--alignment-right & {
text-align: right;
}
}

View file

@ -1,5 +1,5 @@
.paragraph-element {
font-size: 14px;
padding: 2px 82px;
margin: 0;
color: $black;
}

View file

@ -0,0 +1,5 @@
.page-root-element__inner {
padding: 0 20px;
margin: 0 auto;
max-width: $builder-page-max-width;
}

View file

@ -0,0 +1,3 @@
.public-page {
background-color: $white;
}

View file

@ -102,3 +102,5 @@ $file-field-modal-body-nav-width: 120px !default;
$file-field-modal-foot-height: 108px !default;
$dashboard-breakpoint: 1100px;
$builder-page-max-width: 1280px;

View file

@ -524,5 +524,12 @@
},
"dropdown": {
"empty": "No items available"
},
"styleSidePanel": {
"paddingTop": "Padding top",
"paddingBottom": "Padding bottom"
},
"styleBoxForm": {
"paddingError": "The value must be an integer between 0 and 200."
}
}