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/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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
<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>
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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"],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue