Replace RichText editor with spectrum text area

This commit is contained in:
Andrew Kingston 2021-04-16 16:00:10 +01:00
parent 7ba5ff7b34
commit 691c9a9bd1
11 changed files with 91 additions and 255 deletions

View File

@ -62,10 +62,7 @@
"@spectrum-css/underlay": "^2.0.9", "@spectrum-css/underlay": "^2.0.9",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"markdown-it": "^12.0.4",
"quill": "^1.3.7",
"svelte-flatpickr": "^2.4.0", "svelte-flatpickr": "^2.4.0",
"svelte-portal": "^1.0.0", "svelte-portal": "^1.0.0"
"turndown": "^7.0.0"
} }
} }

View File

@ -0,0 +1,48 @@
<script>
import "@spectrum-css/textfield/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
export let value = ""
export let placeholder = null
export let disabled = false
export let error = null
export let id = null
export let updateOnChange = true
const dispatch = createEventDispatcher()
const onChange = event => {
dispatch("change", event.target.value)
}
</script>
<div
class="spectrum-Textfield spectrum-Textfield--multiline"
class:is-invalid={!!error}
class:is-disabled={disabled}>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<textarea
placeholder={placeholder || ''}
class="spectrum-Textfield-input"
{disabled}
{id}
on:blur={onChange}
on:change={updateOnChange ? onChange : null}>{value || ''}</textarea>
</div>
<style>
.spectrum-Textfield {
width: 100%;
}
textarea {
resize: vertical;
min-height: 80px !important;
}
</style>

View File

@ -3,3 +3,4 @@ export { default as CoreSelect } from "./Select.svelte"
export { default as CoreMultiselect } from "./Multiselect.svelte" export { default as CoreMultiselect } from "./Multiselect.svelte"
export { default as CoreCheckbox } from "./Checkbox.svelte" export { default as CoreCheckbox } from "./Checkbox.svelte"
export { default as CoreRadioGroup } from "./RadioGroup.svelte" export { default as CoreRadioGroup } from "./RadioGroup.svelte"
export { default as CoreTextArea } from "./TextArea.svelte"

View File

@ -4,7 +4,7 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let value = null export let value = null
export let label = undefined export let label = null
export let disabled = false export let disabled = false
export let labelPosition = "above" export let labelPosition = "above"
export let error = null export let error = null

View File

@ -1,59 +0,0 @@
<script>
import * as Quill from "quill"
import * as MarkdownIt from "markdown-it"
import TurndownService from "turndown"
import { onMount } from "svelte"
import "quill/dist/quill.snow.css"
const convertMarkdown = new MarkdownIt()
convertMarkdown.set({
html: true,
})
const turndownService = new TurndownService()
export let value = ""
export let options = null
export let width = 400
let quill
let container
let defaultOptions = {
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
["bold", "italic", "underline", "strike"],
],
},
placeholder: "Type something...",
theme: "snow", // or 'bubble'
}
let mergedOptions = { ...defaultOptions, ...options }
const updateContent = () => {
value = turndownService.turndown(quill.container.firstChild.innerHTML)
}
onMount(() => {
quill = new Quill(container, mergedOptions)
if (value)
quill.clipboard.dangerouslyPasteHTML(convertMarkdown.render(value + "\n"))
quill.on("text-change", updateContent)
return () => {
quill.off("text-change", updateContent)
}
})
</script>
<svelte:head>
{#if mergedOptions.theme !== 'snow'}
<link
rel="stylesheet"
href="//cdn.quilljs.com/1.3.6/quill.{mergedOptions.theme}.css" />
{/if}
</svelte:head>
<div style="width: {width}px">
<div bind:this={container} />
</div>

View File

@ -1,132 +1,29 @@
<script> <script>
import Field from "./Field.svelte"
import TextArea from "./Core/TextArea.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import Button from "../Button/Button.svelte"
import Label from "../Styleguide/Label.svelte" export let value = null
import text_area_resize from "../Actions/autoresize_textarea.js" export let label = null
export let labelPosition = "above"
export let placeholder = null
export let disabled = false
export let error = null
export let updateOnChange = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => {
export let name = false dispatch("change", e.detail)
export let label = false value = e.detail
export let thin = false
export let extraThin = false
export let edit = false
export let disabled = false
export let placeholder
export let validator = () => {}
export let value = ""
export const getCaretPosition = () => {
return { start: textarea.selectionStart, end: textarea.selectionEnd }
}
let textarea
// This section handles the edit mode and dispatching of things to the parent when saved
let editMode = false
const save = () => {
editMode = false
dispatch("save", value)
}
const enableEdit = () => {
editMode = true
} }
</script> </script>
<div class="container"> <Field {label} {labelPosition} {disabled} {error}>
{#if label || edit} <TextArea
<div class="label-container"> {error}
{#if label} {disabled}
<Label extraSmall grey forAttr={name}>{label}</Label> {value}
{/if}
{#if edit}
<div class="controls">
<Button small secondary disabled={editMode} on:click={enableEdit}>
Edit
</Button>
<Button small blue disabled={!editMode} on:click={save}>Save</Button>
</div>
{/if}
</div>
{/if}
<textarea
class:thin
class:extraThin
bind:value
bind:this={textarea}
on:change
disabled={disabled || (edit && !editMode)}
{placeholder} {placeholder}
{name} {updateOnChange}
use:text_area_resize /> on:change={onChange} />
</div> </Field>
<style>
.container {
min-width: 0;
display: flex;
flex-direction: column;
}
.label-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
margin-bottom: var(--spacing-s);
}
.label-container :global(label) {
margin-bottom: 0;
}
.controls {
align-items: center;
display: grid;
grid-template-columns: auto auto;
grid-gap: 12px;
margin-left: auto;
padding-left: 12px;
}
.controls :global(button) {
min-width: 100px;
font-size: var(--font-size-s);
border-radius: var(--rounded-small);
}
textarea {
min-width: 0;
color: var(--ink);
font-size: var(--font-size-s);
font-family: var(--font-sans);
border: none;
border-radius: var(--border-radius-s);
background-color: var(--grey-2);
padding: var(--spacing-m);
margin: 0;
border: var(--border-transparent);
outline: none;
}
textarea::placeholder {
color: var(--grey-6);
}
textarea.thin {
font-size: var(--font-size-xs);
}
textarea.extraThin {
font-size: var(--font-size-xs);
padding: var(--spacing-s) var(--spacing-m);
}
textarea:focus {
border: var(--border-blue);
}
textarea:disabled {
background: var(--grey-4);
}
textarea:disabled {
background: var(--grey-4);
}
textarea:disabled::placeholder {
color: var(--grey-6);
}
</style>

View File

@ -32,7 +32,7 @@
async function focusFirstInput(node) { async function focusFirstInput(node) {
const inputs = node.querySelectorAll("input") const inputs = node.querySelectorAll("input")
if (inputs) { if (inputs?.length) {
await tick() await tick()
inputs[0].focus() inputs[0].focus()
} }

View File

@ -3,7 +3,6 @@ import "./bbui.css"
// Components // Components
export { default as Input } from "./Form/Input.svelte" export { default as Input } from "./Form/Input.svelte"
export { default as TextArea } from "./Form/TextArea.svelte" export { default as TextArea } from "./Form/TextArea.svelte"
export { default as RichText } from "./Form/RichText.svelte"
export { default as Select } from "./Form/Select.svelte" export { default as Select } from "./Form/Select.svelte"
export { default as DataList } from "./Form/DataList.svelte" export { default as DataList } from "./Form/DataList.svelte"
export { default as Dropzone } from "./Dropzone/Dropzone.svelte" export { default as Dropzone } from "./Dropzone/Dropzone.svelte"

View File

@ -5,7 +5,7 @@
Label, Label,
DatePicker, DatePicker,
Toggle, Toggle,
RichText, TextArea,
} from "@budibase/bbui" } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte" import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers" import { capitalise } from "../../../helpers"
@ -40,10 +40,7 @@
<LinkedRowSelector bind:linkedRows={value} schema={meta} /> <LinkedRowSelector bind:linkedRows={value} schema={meta} />
</div> </div>
{:else if type === 'longform'} {:else if type === 'longform'}
<div> <TextArea {label} bind:value />
<Label extraSmall grey>{label}</Label>
<RichText bind:value />
</div>
{:else} {:else}
<Input <Input
{label} {label}

View File

@ -1,6 +1,5 @@
<script> <script>
import { onMount } from "svelte" import { CoreTextArea } from "@budibase/bbui"
import { RichText } from "@budibase/bbui"
import Field from "./Field.svelte" import Field from "./Field.svelte"
export let field export let field
@ -10,33 +9,6 @@
let fieldState let fieldState
let fieldApi let fieldApi
// Update form value from bound value after we've mounted
let value
let mounted = false
$: mounted && fieldApi?.setValue(value)
// Get the fields initial value after initialising
onMount(() => {
value = $fieldState?.value
mounted = true
})
// Options for rich text component
const options = {
modules: {
toolbar: [
[
{
header: [1, 2, 3, false],
},
],
["bold", "italic", "underline", "strike"],
],
},
placeholder: placeholder || "Type something...",
theme: "snow",
}
</script> </script>
<Field <Field
@ -47,35 +19,14 @@
bind:fieldState bind:fieldState
bind:fieldApi bind:fieldApi
defaultValue=""> defaultValue="">
{#if mounted} {#if fieldState}
<div class:disabled={$fieldState.disabled}> <CoreTextArea
<RichText bind:value {options} /> value={$fieldState.value}
</div> on:change={e => fieldApi.setValue(e.detail)}
updateOnChange={false}
disabled={$fieldState.disabled}
error={$fieldState.error}
id={$fieldState.fieldId}
{placeholder} />
{/if} {/if}
</Field> </Field>
<style>
div {
background-color: white;
}
div :global(> div) {
width: auto !important;
}
div :global(.ql-snow.ql-toolbar:after, .ql-snow .ql-toolbar:after) {
display: none;
}
div :global(.ql-snow .ql-formats:after) {
display: none;
}
div :global(.ql-editor p) {
word-break: break-all;
}
div.disabled {
pointer-events: none !important;
background-color: rgb(244, 244, 244);
}
div.disabled :global(.ql-container *) {
color: var(--spectrum-alias-text-color-disabled) !important;
}
</style>

View File

@ -12,7 +12,12 @@ export default ({ mode }) => {
}, },
minify: isProduction, minify: isProduction,
}, },
plugins: [svelte()], plugins: [
svelte({
hot: !isProduction,
emitCss: true,
}),
],
resolve: { resolve: {
dedupe: ["svelte", "svelte/internal"], dedupe: ["svelte", "svelte/internal"],
}, },