Improve logic around swapping binding panel tabs

This commit is contained in:
Andrew Kingston 2024-03-05 18:38:48 +00:00
parent 65ca394f61
commit 5b3280832c
5 changed files with 141 additions and 70 deletions

View File

@ -40,20 +40,20 @@
indentMore,
indentLess,
} from "@codemirror/commands"
import { Compartment } from "@codemirror/state"
import { Compartment, EditorState } from "@codemirror/state"
import { javascript } from "@codemirror/lang-javascript"
import { EditorModes } from "./"
import { themeStore } from "stores/portal"
export let label
export let completions = []
export let resize = "none"
export let mode = EditorModes.Handlebars
export let value = ""
export let placeholder = null
export let autocompleteEnabled = true
export let autofocus = false
export let jsBindingWrapping = true
export let readonly = false
// Export a function to expose caret position
export const getCaretPosition = () => {
@ -143,32 +143,21 @@
const buildBaseExtensions = () => {
return [
...(mode.name === "handlebars" ? [plugin] : []),
history(),
drawSelection(),
dropCursor(),
bracketMatching(),
closeBrackets(),
highlightActiveLine(),
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
highlightActiveLineGutter(),
highlightSpecialChars(),
lineNumbers(),
foldGutter(),
EditorView.lineWrapping,
EditorView.updateListener.of(v => {
const docStr = v.state.doc?.toString()
if (docStr === value) {
return
}
dispatch("change", docStr)
}),
keymap.of(buildKeymap()),
themeConfig.of([...(isDark ? [oneDark] : [])]),
]
}
// None of this is reactive, but it never has been, so we just assume most
// config flags aren't changed at runtime
const buildExtensions = base => {
const complete = [...base]
let complete = [...base]
if (autocompleteEnabled) {
complete.push(
@ -210,12 +199,36 @@
if (mode.name === "javascript") {
complete.push(javascript())
complete.push(highlightWhitespace())
if (!readonly) {
complete.push(highlightWhitespace())
}
}
if (placeholder) {
complete.push(placeholderFn(placeholder))
}
if (readonly) {
complete.push(EditorState.readOnly.of(true))
} else {
complete = [
...complete,
history(),
highlightActiveLine(),
highlightActiveLineGutter(),
lineNumbers(),
foldGutter(),
keymap.of(buildKeymap()),
EditorView.updateListener.of(v => {
const docStr = v.state.doc?.toString()
if (docStr === value) {
return
}
dispatch("change", docStr)
}),
]
}
return complete
}
@ -301,7 +314,6 @@
/* Active line */
.code-editor :global(.cm-line) {
height: 16px;
padding: 0 var(--spacing-s);
color: var(--spectrum-alias-text-color);
}
@ -319,6 +331,9 @@
background: var(--spectrum-global-color-gray-100) !important;
z-index: -2;
}
.code-editor :global(.cm-highlightSpace:before) {
color: var(--spectrum-global-color-gray-500);
}
/* Code selection */
.code-editor :global(.cm-selectionBackground) {

View File

@ -25,6 +25,7 @@
} from "../CodeEditor"
import BindingSidePanel from "./BindingSidePanel.svelte"
import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
import SnippetSidePanel from "./SnippetSidePanel.svelte"
import { BindingHelpers } from "./utils"
import formatHighlight from "json-format-highlight"
import { capitalise } from "helpers"
@ -38,6 +39,7 @@
export let valid
export let allowJS = false
export let allowHelpers = true
export let allowSnippets = true
export let context = null
export let autofocusEditor = false
@ -49,6 +51,7 @@
const SidePanels = {
Bindings: "FlashOn",
Evaluation: "Play",
Snippets: "Code",
}
let initialValueJS = value?.startsWith?.("{{ js ")
@ -64,10 +67,8 @@
let evaluating = false
$: drawerContext?.modal.subscribe(val => (drawerIsModal = val))
$: editorTabs = allowJS ? [Modes.Text, Modes.JavaScript] : [Modes.Text]
$: sideTabs = context
? [SidePanels.Evaluation, SidePanels.Bindings]
: [SidePanels.Bindings]
$: editorModeOptions = allowJS ? [Modes.Text, Modes.JavaScript] : [Modes.Text]
$: sidePanelOptions = getSidePanelOptions(context, allowSnippets, mode)
$: enrichedBindings = enrichBindings(bindings, context)
$: usingJS = mode === Modes.JavaScript
$: editorMode =
@ -77,6 +78,22 @@
$: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value)
$: requestUpdateEvaluation(runtimeExpression, context)
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
$: {
if (!sidePanelOptions.includes(sidePanel)) {
sidePanel = SidePanels.Bindings
}
}
const getSidePanelOptions = (context, allowSnippets, mode) => {
let options = [SidePanels.Bindings]
if (context) {
options.unshift(SidePanels.Evaluation)
}
if (allowSnippets && mode === Modes.JavaScript) {
options.push(SidePanels.Snippets)
}
return options
}
const debouncedUpdateEvaluation = Utils.debounce((expression, context) => {
expressionResult = processStringSync(expression || "", context)
@ -135,11 +152,22 @@
bindingHelpers.onSelectBinding(js ? jsValue : hbsValue, binding, { js })
}
const onSelectSnippet = snippet => {
bindingHelpers.onSelectSnippet(jsValue, snippet)
}
const changeMode = newMode => {
if (targetMode || newMode === mode) {
return
}
if (editorValue) {
// Get the raw editor value to see if we are abandoning changes
let rawValue = editorValue
if (mode === Modes.JavaScript) {
rawValue = decodeJSBinding(rawValue)
}
if (rawValue?.length) {
targetMode = newMode
} else {
mode = newMode
@ -178,26 +206,26 @@
<div class="main">
<div class="tabs">
<div class="editor-tabs">
{#each editorTabs as tab}
{#each editorModeOptions as editorMode}
<ActionButton
size="M"
quiet
selected={mode === tab}
on:click={() => changeMode(tab)}
selected={mode === editorMode}
on:click={() => changeMode(editorMode)}
>
{capitalise(tab)}
{capitalise(editorMode)}
</ActionButton>
{/each}
</div>
<div class="side-tabs">
{#each sideTabs as tab}
{#each sidePanelOptions as panel}
<ActionButton
size="M"
quiet
selected={sidePanel === tab}
on:click={() => changeSidePanel(tab)}
selected={sidePanel === panel}
on:click={() => changeSidePanel(panel)}
>
<Icon name={tab} size="S" />
<Icon name={panel} size="S" />
</ActionButton>
{/each}
{#if drawerContext && get(drawerContext.resizable)}
@ -287,6 +315,8 @@
{evaluating}
expression={editorValue}
/>
{:else if sidePanel === SidePanels.Snippets}
<SnippetSidePanel addSnippet={onSelectSnippet} />
{/if}
</div>
</div>

View File

@ -1,5 +1,7 @@
<script>
import { Input, Layout, Icon, Popover } from "@budibase/bbui"
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
import { EditorModes } from "components/common/CodeEditor"
export let addSnippet
export let snippets
@ -15,16 +17,36 @@
{
name: "Square",
code: `
return function(num) {
return num * num
}
`,
return function(num) {
return num * num
}
`,
},
{
name: "HelloWorld",
code: `
return "Hello, world!"
`,
return "Hello, world!"
`,
},
{
name: "Colorful",
code: `
let a = null
let b = "asdasd"
let c = 123123
let d = undefined
let e = [1, 2, 3]
let f = { foo: "bar" }
let g = Math.round(1.234)
if (a === b) {
return c ?? e
}
return d || f
// comment
let h = 1 + 2 + 3 * 3
let i = true
let j = false
`,
},
]
@ -89,8 +111,15 @@
on:mouseenter={stopHidingPopover}
on:mouseleave={hidePopover}
>
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
<pre class="snippet-popover">{@html hoveredSnippet.code}</pre>
<div class="snippet-popover">
{#key hoveredSnippet}
<CodeEditor
value={hoveredSnippet.code.trim()}
mode={EditorModes.JS}
readonly
/>
{/key}
</div>
</Popover>
<div class="snippet-side-panel">
@ -114,15 +143,13 @@
<div class="snippet-list">
{#each filteredSnippets as snippet}
<div class="snippet">
<div
class="snippet-name"
on:mouseenter={e => showSnippet(snippet, e.target)}
on:mouseleave={hidePopover}
on:click={() => addSnippet(snippet)}
>
{snippet.name}
</div>
<div
class="snippet"
on:mouseenter={e => showSnippet(snippet, e.target)}
on:mouseleave={hidePopover}
on:click={() => addSnippet(snippet)}
>
{snippet.name}
</div>
{/each}
</div>
@ -155,14 +182,18 @@
background: none;
padding: 0;
}
.search-input {
flex: 1;
.search-input,
.title {
flex: 1 1 auto;
}
/* List */
.snippet-list {
padding: 0 var(--spacing-l);
padding-bottom: var(--spacing-l);
display: flex;
flex-direction: column;
gap: var(--spacing-s);
}
.snippet {
font-size: var(--font-size-s);
@ -173,14 +204,14 @@
border-color 130ms ease-out;
word-wrap: break-word;
}
.snippet:hover {
color: var(--spectrum-global-color-gray-900);
background-color: var(--spectrum-global-color-gray-50);
cursor: pointer;
}
/* Popover */
.snippet-popover {
padding: 0;
margin: 0;
font-size: 12px;
white-space: pre;
text-overflow: ellipsis;
overflow: hidden;
width: 400px;
}
</style>

View File

@ -38,4 +38,13 @@ export class BindingHelpers {
this.insertAtPos({ start, end, value: insertVal })
}
}
// Adds a JS/HBS helper to the expression
onSelectSnippet(value, snippet) {
const pos = this.getCaretPosition()
const { start, end } = pos
const jsVal = decodeJSBinding(value)
const insertVal = jsInsert(jsVal, start, end, `snippets.${snippet.name}`)
this.insertAtPos({ start, end, value: insertVal })
}
}

View File

@ -41,22 +41,8 @@ const getContextValue = (path, context) => {
return data
}
const snippets = {
Square: `
return function(num) {
return num * num
}
`,
HelloWorld: `
return "Hello, world!"
`,
}
// Evaluates JS code against a certain context
module.exports.processJS = (handlebars, context) => {
// for testing
context.snippets = snippets
if (!isJSAllowed() || (isBackendService() && !runJS)) {
throw new Error("JS disabled in environment.")
}