Work on binding drawer typing.

This commit is contained in:
mike12345567 2025-01-15 14:34:36 +00:00
parent 2fdf2289ed
commit 0b909b434a
5 changed files with 95 additions and 65 deletions

View File

@ -4,7 +4,7 @@
export let size = "M" export let size = "M"
export let tooltip = "" export let tooltip = ""
export let muted export let muted = undefined
</script> </script>
<TooltipWrapper {tooltip} {size}> <TooltipWrapper {tooltip} {size}>

View File

@ -1,4 +1,4 @@
<script> <script lang="ts">
import { Label } from "@budibase/bbui" import { Label } from "@budibase/bbui"
import { onMount, createEventDispatcher, onDestroy } from "svelte" import { onMount, createEventDispatcher, onDestroy } from "svelte"
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates" import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
@ -12,7 +12,6 @@
completionStatus, completionStatus,
} from "@codemirror/autocomplete" } from "@codemirror/autocomplete"
import { import {
EditorView,
lineNumbers, lineNumbers,
keymap, keymap,
highlightSpecialChars, highlightSpecialChars,
@ -25,6 +24,7 @@
MatchDecorator, MatchDecorator,
ViewPlugin, ViewPlugin,
Decoration, Decoration,
EditorView,
} from "@codemirror/view" } from "@codemirror/view"
import { import {
bracketMatching, bracketMatching,
@ -44,12 +44,14 @@
import { javascript } from "@codemirror/lang-javascript" import { javascript } from "@codemirror/lang-javascript"
import { EditorModes } from "./" import { EditorModes } from "./"
import { themeStore } from "@/stores/portal" import { themeStore } from "@/stores/portal"
import type { EditorMode } from "@budibase/types"
export let label export let label: string | undefined = undefined
export let completions = [] // TODO: work out what best type fits this
export let mode = EditorModes.Handlebars export let completions: any[] = []
export let value = "" export let mode: EditorMode = EditorModes.Handlebars
export let placeholder = null export let value: string | null = ""
export let placeholder: string | null = null
export let autocompleteEnabled = true export let autocompleteEnabled = true
export let autofocus = false export let autofocus = false
export let jsBindingWrapping = true export let jsBindingWrapping = true
@ -58,8 +60,8 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let textarea let textarea: HTMLDivElement | undefined
let editor let editor: EditorView | undefined
let mounted = false let mounted = false
let isEditorInitialised = false let isEditorInitialised = false
let queuedRefresh = false let queuedRefresh = false
@ -71,7 +73,7 @@
$: { $: {
if (autofocus && isEditorInitialised) { if (autofocus && isEditorInitialised) {
editor.focus() editor?.focus()
} }
} }
@ -88,7 +90,7 @@
isDark = !currentTheme.includes("light") isDark = !currentTheme.includes("light")
// Issue theme compartment update // Issue theme compartment update
editor.dispatch({ editor?.dispatch({
effects: themeConfig.reconfigure([...(isDark ? [oneDark] : [])]), effects: themeConfig.reconfigure([...(isDark ? [oneDark] : [])]),
}) })
} }
@ -100,17 +102,24 @@
/** /**
* Will refresh the editor contents only after * Will refresh the editor contents only after
* it has been fully initialised * it has been fully initialised
* @param value {string} the editor value
*/ */
const refresh = (value, initialised, mounted) => { const refresh = (
value: string | null,
initialised?: boolean,
mounted?: boolean
) => {
if (!initialised || !mounted) { if (!initialised || !mounted) {
queuedRefresh = true queuedRefresh = true
return return
} }
if (editor.state.doc.toString() !== value || queuedRefresh) { if (
editor &&
value &&
(editor.state.doc.toString() !== value || queuedRefresh)
) {
editor.dispatch({ editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: value }, changes: { from: 0, to: editor.state.doc.length, insert: value! },
}) })
queuedRefresh = false queuedRefresh = false
} }
@ -118,17 +127,22 @@
// Export a function to expose caret position // Export a function to expose caret position
export const getCaretPosition = () => { export const getCaretPosition = () => {
const selection_range = editor.state.selection.ranges[0] const selection_range = editor?.state.selection.ranges[0]
return { return {
start: selection_range.from, start: selection_range?.from,
end: selection_range.to, end: selection_range?.to,
} }
} }
export const insertAtPos = opts => { export const insertAtPos = (opts: {
start: number
end?: number
value: string
cursor: { anchor: number }
}) => {
// Updating the value inside. // Updating the value inside.
// Retain focus // Retain focus
editor.dispatch({ editor?.dispatch({
changes: { changes: {
from: opts.start || editor.state.doc.length, from: opts.start || editor.state.doc.length,
to: opts.end || editor.state.doc.length, to: opts.end || editor.state.doc.length,
@ -192,7 +206,7 @@
const indentWithTabCustom = { const indentWithTabCustom = {
key: "Tab", key: "Tab",
run: view => { run: (view: EditorView) => {
if (completionStatus(view.state) === "active") { if (completionStatus(view.state) === "active") {
acceptCompletion(view) acceptCompletion(view)
return true return true
@ -200,7 +214,7 @@
indentMore(view) indentMore(view)
return true return true
}, },
shift: view => { shift: (view: EditorView) => {
indentLess(view) indentLess(view)
return true return true
}, },
@ -232,7 +246,8 @@
// None of this is reactive, but it never has been, so we just assume most // None of this is reactive, but it never has been, so we just assume most
// config flags aren't changed at runtime // config flags aren't changed at runtime
const buildExtensions = base => { // TODO: work out type for base
const buildExtensions = (base: any[]) => {
let complete = [...base] let complete = [...base]
if (autocompleteEnabled) { if (autocompleteEnabled) {
@ -242,7 +257,7 @@
closeOnBlur: true, closeOnBlur: true,
icons: false, icons: false,
optionClass: completion => optionClass: completion =>
completion.simple "simple" in completion && completion.simple
? "autocomplete-option-simple" ? "autocomplete-option-simple"
: "autocomplete-option", : "autocomplete-option",
}) })
@ -308,7 +323,7 @@
keymap.of(buildKeymap()), keymap.of(buildKeymap()),
EditorView.domEventHandlers({ EditorView.domEventHandlers({
blur: () => { blur: () => {
dispatch("blur", editor.state.doc.toString()) dispatch("blur", editor?.state.doc.toString())
}, },
}), }),
EditorView.updateListener.of(v => { EditorView.updateListener.of(v => {
@ -347,7 +362,7 @@
{#if label} {#if label}
<div> <div>
<Label small>{label}</Label> <Label size="S">{label}</Label>
</div> </div>
{/if} {/if}

View File

@ -31,20 +31,21 @@
import { capitalise } from "@/helpers" import { capitalise } from "@/helpers"
import { Utils, JsonFormatter } from "@budibase/frontend-core" import { Utils, JsonFormatter } from "@budibase/frontend-core"
import { licensing } from "@/stores/portal" import { licensing } from "@/stores/portal"
import { import { BindingMode, EditorMode, SidePanel } from "@budibase/types"
import type {
EnrichedBinding, EnrichedBinding,
BindingMode,
SidePanel,
BindingCompletion, BindingCompletion,
Snippet, Snippet,
Helper, Helper,
CaretPositionFn,
InsertAtPositionFn,
} from "@budibase/types" } from "@budibase/types"
import { CompletionContext } from "@codemirror/autocomplete" import type { CompletionContext } from "@codemirror/autocomplete"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let bindings: EnrichedBinding[] = [] export let bindings: EnrichedBinding[] = []
export let value = "" export let value: string = ""
export let allowHBS = true export let allowHBS = true
export let allowJS = false export let allowJS = false
export let allowHelpers = true export let allowHelpers = true
@ -58,14 +59,10 @@
let mode: BindingMode | null let mode: BindingMode | null
let sidePanel: SidePanel | null let sidePanel: SidePanel | null
let initialValueJS = value?.startsWith?.("{{ js ") let initialValueJS = value?.startsWith?.("{{ js ")
let jsValue = initialValueJS ? value : null let jsValue: string | null = initialValueJS ? value : null
let hbsValue = initialValueJS ? null : value let hbsValue: string | null = initialValueJS ? null : value
let getCaretPosition: () => { start: number; end: number } | undefined let getCaretPosition: CaretPositionFn | undefined
let insertAtPos: (_: { let insertAtPos: InsertAtPositionFn | undefined
start: number
end: number
value: string
}) => void | undefined
let targetMode: BindingMode | null = null let targetMode: BindingMode | null = null
let expressionResult: string | undefined | null let expressionResult: string | undefined | null
let expressionError: string | undefined | null let expressionError: string | undefined | null
@ -83,7 +80,9 @@
$: usingJS = mode === BindingMode.JavaScript $: usingJS = mode === BindingMode.JavaScript
$: editorMode = $: editorMode =
mode === BindingMode.JavaScript ? EditorModes.JS : EditorModes.Handlebars mode === BindingMode.JavaScript ? EditorModes.JS : EditorModes.Handlebars
$: editorValue = editorMode === EditorModes.JS ? jsValue : hbsValue $: editorValue = (editorMode === EditorModes.JS ? jsValue : hbsValue) as
| string
| null
$: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value) $: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value)
$: requestEval(runtimeExpression, context, snippets) $: requestEval(runtimeExpression, context, snippets)
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode) $: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
@ -300,6 +299,9 @@
} }
} }
const addSnippet = (snippet: Snippet) =>
bindingHelpers.onSelectSnippet(snippet)
onMount(() => { onMount(() => {
// Set the initial mode appropriately // Set the initial mode appropriately
const initialValueMode = initialValueJS const initialValueMode = initialValueJS
@ -365,7 +367,7 @@
{:else if mode === BindingMode.JavaScript} {:else if mode === BindingMode.JavaScript}
{#key jsCompletions} {#key jsCompletions}
<CodeEditor <CodeEditor
value={decodeJSBinding(jsValue)} value={jsValue ? decodeJSBinding(jsValue) : jsValue}
on:change={onChangeJSValue} on:change={onChangeJSValue}
completions={jsCompletions} completions={jsCompletions}
mode={EditorModes.JS} mode={EditorModes.JS}
@ -419,13 +421,10 @@
{expressionResult} {expressionResult}
{expressionError} {expressionError}
{evaluating} {evaluating}
expression={editorValue} expression={editorValue ? editorValue : ""}
/> />
{:else if sidePanel === SidePanel.Snippets} {:else if sidePanel === SidePanel.Snippets}
<SnippetSidePanel <SnippetSidePanel {addSnippet} {snippets} />
addSnippet={snippet => bindingHelpers.onSelectSnippet(snippet)}
{snippets}
/>
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -15,3 +15,12 @@ export enum BindingMode {
Text = "Text", Text = "Text",
JavaScript = "JavaScript", JavaScript = "JavaScript",
} }
export type CaretPositionFn = () => { start: number; end: number }
export type InsertAtPositionFn = (_: {
start: number
end?: number
value: string
cursor?: { anchor: number }
}) => void

View File

@ -1,19 +1,26 @@
type EditorMode = interface JSEditorMode {
| { name: "javascript"
key: "JS" json: boolean
name: "javascript" match: RegExp
json: boolean }
match: RegExp
}
| {
key: "Handlebars"
name: "handlebars"
base: "text/html"
match: RegExp
}
| {
key: "Text"
name: "text/html"
}
export type EditorModesMap = { [M in EditorMode as M["key"]]: Omit<M, "key"> } interface HBSEditorMode {
name: "handlebars"
base: "text/html"
match: RegExp
}
interface HTMLEditorMode {
name: "text/html"
}
export type EditorMode = JSEditorMode | HBSEditorMode | HTMLEditorMode
type EditorModeMapBase =
| (JSEditorMode & { key: "JS" })
| (HBSEditorMode & { key: "Handlebars" })
| (HTMLEditorMode & { key: "Text" })
export type EditorModesMap = {
[M in EditorModeMapBase as M["key"]]: Omit<M, "key">
}