Allow in-preview editing of paragraphs and headings

This commit is contained in:
Andrew Kingston 2021-10-28 12:43:31 +01:00
parent c6304ca321
commit b522726afc
8 changed files with 88 additions and 33 deletions

View File

@ -589,6 +589,7 @@
"icon": "TextParagraph", "icon": "TextParagraph",
"illegalChildren": ["section"], "illegalChildren": ["section"],
"showSettingsBar": true, "showSettingsBar": true,
"editable": true,
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -695,6 +696,7 @@
"description": "A component for displaying heading text", "description": "A component for displaying heading text",
"illegalChildren": ["section"], "illegalChildren": ["section"],
"showSettingsBar": true, "showSettingsBar": true,
"editable": true,
"settings": [ "settings": [
{ {
"type": "text", "type": "text",

View File

@ -60,21 +60,32 @@
$: instanceKey = JSON.stringify(rawProps) $: instanceKey = JSON.stringify(rawProps)
$: updateComponentProps(rawProps, instanceKey, $context) $: updateComponentProps(rawProps, instanceKey, $context)
$: selected = $: selected =
$builderStore.inBuilder && $builderStore.inBuilder && $builderStore.selectedComponentId === id
$builderStore.selectedComponentId === instance._id
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id) $: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
$: evaluateConditions(enrichedSettings?._conditions) $: evaluateConditions(enrichedSettings?._conditions)
$: componentSettings = { ...enrichedSettings, ...conditionalSettings } $: componentSettings = { ...enrichedSettings, ...conditionalSettings }
$: renderKey = `${propsHash}-${emptyState}` $: renderKey = `${propsHash}-${emptyState}`
$: editable = definition.editable
$: editing = editable && selected && $builderStore.editMode
$: draggable = interactive && !isLayout && !isScreen && !editing
$: droppable = interactive && !isLayout && !isScreen
// Update component context // Update component context
$: componentStore.set({ $: componentStore.set({
id, id,
children: children.length, children: children.length,
styles: { ...instance._styles, id, empty: emptyState, interactive }, styles: {
...instance._styles,
id,
empty: emptyState,
interactive,
draggable,
editable,
},
empty: emptyState, empty: emptyState,
selected, selected,
name, name,
editing,
}) })
const getRawProps = instance => { const getRawProps = instance => {
@ -171,10 +182,6 @@
conditionalSettings = result.settingUpdates conditionalSettings = result.settingUpdates
visible = nextVisible visible = nextVisible
} }
// Drag and drop helper tags
$: draggable = interactive && !isLayout && !isScreen
$: droppable = interactive && !isLayout && !isScreen
</script> </script>
{#key renderKey} {#key renderKey}
@ -187,6 +194,7 @@
class:droppable class:droppable
class:empty class:empty
class:interactive class:interactive
class:editing
data-id={id} data-id={id}
data-name={name} data-name={name}
> >
@ -213,4 +221,7 @@
.draggable :global(*:hover) { .draggable :global(*:hover) {
cursor: grab; cursor: grab;
} }
.editing :global(*:hover) {
cursor: auto;
}
</style> </style>

View File

@ -36,21 +36,34 @@
}, },
} }
} }
// Convert contenteditable HTML to text and save
const updateText = e => {
const html = e.target.innerHTML
const sanitized = html
.replace(/<\/div><div>/gi, "\n")
.replace(/<div>/gi, "")
.replace(/<\/div>/gi, "")
.replace(/<br>/gi, "")
builderStore.actions.updateProp("text", sanitized)
}
</script> </script>
<h1 <div
contenteditable={$component.editing}
use:styleable={styles} use:styleable={styles}
class:placeholder class:placeholder
class:bold class:bold
class:italic class:italic
class:underline class:underline
class="spectrum-Heading {sizeClass} {alignClass}" class="spectrum-Heading {sizeClass} {alignClass}"
on:blur={$component.editing ? updateText : null}
> >
{componentText} {componentText}
</h1> </div>
<style> <style>
h1 { div {
white-space: pre-wrap; white-space: pre-wrap;
font-weight: 600; font-weight: 600;
} }

View File

@ -35,21 +35,34 @@
}, },
} }
} }
// Convert contenteditable HTML to text and save
const updateText = e => {
const html = e.target.innerHTML
const sanitized = html
.replace(/<\/div><div>/gi, "\n")
.replace(/<div>/gi, "")
.replace(/<\/div>/gi, "")
.replace(/<br>/gi, "")
builderStore.actions.updateProp("text", sanitized)
}
</script> </script>
<p <div
contenteditable={$component.editing}
use:styleable={styles} use:styleable={styles}
class:placeholder class:placeholder
class:bold class:bold
class:italic class:italic
class:underline class:underline
class="spectrum-Body {sizeClass} {alignClass}" class="spectrum-Body {sizeClass} {alignClass}"
on:blur={$component.editing ? updateText : null}
> >
{componentText} {componentText}
</p> </div>
<style> <style>
p { div {
display: inline-block; display: inline-block;
white-space: pre-wrap; white-space: pre-wrap;
margin: 0; margin: 0;

View File

@ -17,10 +17,9 @@
<div <div
in:fade={{ in:fade={{
delay: transition ? 50 : 0, delay: transition ? 130 : 0,
duration: transition ? 130 : 0, duration: transition ? 130 : 0,
}} }}
out:fade={{ duration: transition ? 130 : 0 }}
class="indicator" class="indicator"
class:flipped class:flipped
class:line class:line

View File

@ -1,11 +1,15 @@
<script> <script>
import { builderStore } from "stores" import { builderStore } from "stores"
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
$: color = $builderStore.editMode
? "var(--spectrum-global-color-static-green-500)"
: "var(--spectrum-global-color-static-blue-600)"
</script> </script>
<IndicatorSet <IndicatorSet
componentId={$builderStore.selectedComponentId} componentId={$builderStore.selectedComponentId}
color="var(--spectrum-global-color-static-blue-600)" {color}
zIndex="910" zIndex="910"
transition transition
/> />

View File

@ -1,4 +1,4 @@
import { writable, derived } from "svelte/store" import { writable, derived, get } from "svelte/store"
import Manifest from "manifest.json" import Manifest from "manifest.json"
import { findComponentById, findComponentPathById } from "../utils/components" import { findComponentById, findComponentPathById } from "../utils/components"
import { pingEndUser } from "../api" import { pingEndUser } from "../api"
@ -18,6 +18,7 @@ const createBuilderStore = () => {
layout: null, layout: null,
screen: null, screen: null,
selectedComponentId: null, selectedComponentId: null,
editMode: false,
previewId: null, previewId: null,
previewType: null, previewType: null,
selectedPath: [], selectedPath: [],
@ -54,6 +55,10 @@ const createBuilderStore = () => {
const actions = { const actions = {
selectComponent: id => { selectComponent: id => {
if (id === get(writableStore).selectedComponentId) {
return
}
writableStore.update(state => ({ ...state, editMode: false }))
dispatchEvent("select-component", { id }) dispatchEvent("select-component", { id })
}, },
updateProp: (prop, value) => { updateProp: (prop, value) => {
@ -69,10 +74,7 @@ const createBuilderStore = () => {
pingEndUser() pingEndUser()
}, },
setSelectedPath: path => { setSelectedPath: path => {
writableStore.update(state => { writableStore.update(state => ({ ...state, selectedPath: path }))
state.selectedPath = path
return state
})
}, },
moveComponent: (componentId, destinationComponentId, mode) => { moveComponent: (componentId, destinationComponentId, mode) => {
dispatchEvent("move-component", { dispatchEvent("move-component", {
@ -82,10 +84,10 @@ const createBuilderStore = () => {
}) })
}, },
setDragging: dragging => { setDragging: dragging => {
writableStore.update(state => { writableStore.update(state => ({ ...state, isDragging: dragging }))
state.isDragging = dragging },
return state setEditMode: enabled => {
}) writableStore.update(state => ({ ...state, editMode: enabled }))
}, },
} }
return { return {

View File

@ -22,12 +22,7 @@ export const styleable = (node, styles = {}) => {
let applyNormalStyles let applyNormalStyles
let applyHoverStyles let applyHoverStyles
let selectComponent let selectComponent
let editComponent
// Allow dragging if required
const parent = node.closest(".component")
if (parent && parent.classList.contains("draggable")) {
node.setAttribute("draggable", true)
}
// Creates event listeners and applies initial styles // Creates event listeners and applies initial styles
const setupStyles = (newStyles = {}) => { const setupStyles = (newStyles = {}) => {
@ -46,6 +41,9 @@ export const styleable = (node, styles = {}) => {
...(newStyles.hover || {}), ...(newStyles.hover || {}),
} }
// Allow dragging if required
node.setAttribute("draggable", !!styles.draggable)
// Applies a style string to a DOM node // Applies a style string to a DOM node
const applyStyles = styleString => { const applyStyles = styleString => {
node.style = styleString node.style = styleString
@ -72,13 +70,25 @@ export const styleable = (node, styles = {}) => {
return false return false
} }
// Handler to start editing a component (if applicable) when double
// clicking in the builder preview
editComponent = event => {
if (newStyles.interactive && newStyles.editable) {
builderStore.actions.setEditMode(true)
}
event.preventDefault()
event.stopPropagation()
return false
}
// Add listeners to toggle hover styles // Add listeners to toggle hover styles
node.addEventListener("mouseover", applyHoverStyles) node.addEventListener("mouseover", applyHoverStyles)
node.addEventListener("mouseout", applyNormalStyles) node.addEventListener("mouseout", applyNormalStyles)
// Add builder preview click listener // Add builder preview listeners
if (get(builderStore).inBuilder) { if (get(builderStore).inBuilder) {
node.addEventListener("click", selectComponent, false) node.addEventListener("click", selectComponent, false)
node.addEventListener("dblclick", editComponent, false)
} }
// Apply initial normal styles // Apply initial normal styles
@ -90,9 +100,10 @@ export const styleable = (node, styles = {}) => {
node.removeEventListener("mouseover", applyHoverStyles) node.removeEventListener("mouseover", applyHoverStyles)
node.removeEventListener("mouseout", applyNormalStyles) node.removeEventListener("mouseout", applyNormalStyles)
// Remove builder preview click listener // Remove builder preview listeners
if (get(builderStore).inBuilder) { if (get(builderStore).inBuilder) {
node.removeEventListener("click", selectComponent) node.removeEventListener("click", selectComponent)
node.removeEventListener("dblclick", editComponent)
} }
} }