Replace RichText editor with spectrum text area
This commit is contained in:
parent
431abb53ce
commit
2ab62dc935
|
@ -62,10 +62,7 @@
|
|||
"@spectrum-css/underlay": "^2.0.9",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"markdown-it": "^12.0.4",
|
||||
"quill": "^1.3.7",
|
||||
"svelte-flatpickr": "^2.4.0",
|
||||
"svelte-portal": "^1.0.0",
|
||||
"turndown": "^7.0.0"
|
||||
"svelte-portal": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -3,3 +3,4 @@ export { default as CoreSelect } from "./Select.svelte"
|
|||
export { default as CoreMultiselect } from "./Multiselect.svelte"
|
||||
export { default as CoreCheckbox } from "./Checkbox.svelte"
|
||||
export { default as CoreRadioGroup } from "./RadioGroup.svelte"
|
||||
export { default as CoreTextArea } from "./TextArea.svelte"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let value = null
|
||||
export let label = undefined
|
||||
export let label = null
|
||||
export let disabled = false
|
||||
export let labelPosition = "above"
|
||||
export let error = null
|
||||
|
|
|
@ -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>
|
|
@ -1,132 +1,29 @@
|
|||
<script>
|
||||
import Field from "./Field.svelte"
|
||||
import TextArea from "./Core/TextArea.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Button from "../Button/Button.svelte"
|
||||
import Label from "../Styleguide/Label.svelte"
|
||||
import text_area_resize from "../Actions/autoresize_textarea.js"
|
||||
|
||||
export let value = null
|
||||
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()
|
||||
|
||||
export let name = false
|
||||
export let label = false
|
||||
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
|
||||
const onChange = e => {
|
||||
dispatch("change", e.detail)
|
||||
value = e.detail
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#if label || edit}
|
||||
<div class="label-container">
|
||||
{#if label}
|
||||
<Label extraSmall grey forAttr={name}>{label}</Label>
|
||||
{/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)}
|
||||
<Field {label} {labelPosition} {disabled} {error}>
|
||||
<TextArea
|
||||
{error}
|
||||
{disabled}
|
||||
{value}
|
||||
{placeholder}
|
||||
{name}
|
||||
use:text_area_resize />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{updateOnChange}
|
||||
on:change={onChange} />
|
||||
</Field>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
async function focusFirstInput(node) {
|
||||
const inputs = node.querySelectorAll("input")
|
||||
if (inputs) {
|
||||
if (inputs?.length) {
|
||||
await tick()
|
||||
inputs[0].focus()
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import "./bbui.css"
|
|||
// Components
|
||||
export { default as Input } from "./Form/Input.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 DataList } from "./Form/DataList.svelte"
|
||||
export { default as Dropzone } from "./Dropzone/Dropzone.svelte"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
Label,
|
||||
DatePicker,
|
||||
Toggle,
|
||||
RichText,
|
||||
TextArea,
|
||||
} from "@budibase/bbui"
|
||||
import Dropzone from "components/common/Dropzone.svelte"
|
||||
import { capitalise } from "../../../helpers"
|
||||
|
@ -40,10 +40,7 @@
|
|||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||
</div>
|
||||
{:else if type === 'longform'}
|
||||
<div>
|
||||
<Label extraSmall grey>{label}</Label>
|
||||
<RichText bind:value />
|
||||
</div>
|
||||
<TextArea {label} bind:value />
|
||||
{:else}
|
||||
<Input
|
||||
{label}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { RichText } from "@budibase/bbui"
|
||||
import { CoreTextArea } from "@budibase/bbui"
|
||||
import Field from "./Field.svelte"
|
||||
|
||||
export let field
|
||||
|
@ -10,33 +9,6 @@
|
|||
|
||||
let fieldState
|
||||
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>
|
||||
|
||||
<Field
|
||||
|
@ -47,35 +19,14 @@
|
|||
bind:fieldState
|
||||
bind:fieldApi
|
||||
defaultValue="">
|
||||
{#if mounted}
|
||||
<div class:disabled={$fieldState.disabled}>
|
||||
<RichText bind:value {options} />
|
||||
</div>
|
||||
{#if fieldState}
|
||||
<CoreTextArea
|
||||
value={$fieldState.value}
|
||||
on:change={e => fieldApi.setValue(e.detail)}
|
||||
updateOnChange={false}
|
||||
disabled={$fieldState.disabled}
|
||||
error={$fieldState.error}
|
||||
id={$fieldState.fieldId}
|
||||
{placeholder} />
|
||||
{/if}
|
||||
</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>
|
||||
|
|
|
@ -12,7 +12,12 @@ export default ({ mode }) => {
|
|||
},
|
||||
minify: isProduction,
|
||||
},
|
||||
plugins: [svelte()],
|
||||
plugins: [
|
||||
svelte({
|
||||
hot: !isProduction,
|
||||
emitCss: true,
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
dedupe: ["svelte", "svelte/internal"],
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue