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, indentMore,
indentLess, indentLess,
} from "@codemirror/commands" } from "@codemirror/commands"
import { Compartment } from "@codemirror/state" import { Compartment, EditorState } from "@codemirror/state"
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"
export let label export let label
export let completions = [] export let completions = []
export let resize = "none"
export let mode = EditorModes.Handlebars export let mode = EditorModes.Handlebars
export let value = "" export let value = ""
export let placeholder = null export let placeholder = 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
export let readonly = false
// Export a function to expose caret position // Export a function to expose caret position
export const getCaretPosition = () => { export const getCaretPosition = () => {
@ -143,32 +143,21 @@
const buildBaseExtensions = () => { const buildBaseExtensions = () => {
return [ return [
...(mode.name === "handlebars" ? [plugin] : []), ...(mode.name === "handlebars" ? [plugin] : []),
history(),
drawSelection(), drawSelection(),
dropCursor(), dropCursor(),
bracketMatching(), bracketMatching(),
closeBrackets(), closeBrackets(),
highlightActiveLine(),
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }), syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
highlightActiveLineGutter(),
highlightSpecialChars(), highlightSpecialChars(),
lineNumbers(),
foldGutter(),
EditorView.lineWrapping, 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] : [])]), 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 buildExtensions = base => {
const complete = [...base] let complete = [...base]
if (autocompleteEnabled) { if (autocompleteEnabled) {
complete.push( complete.push(
@ -210,12 +199,36 @@
if (mode.name === "javascript") { if (mode.name === "javascript") {
complete.push(javascript()) complete.push(javascript())
complete.push(highlightWhitespace()) if (!readonly) {
complete.push(highlightWhitespace())
}
} }
if (placeholder) { if (placeholder) {
complete.push(placeholderFn(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 return complete
} }
@ -301,7 +314,6 @@
/* Active line */ /* Active line */
.code-editor :global(.cm-line) { .code-editor :global(.cm-line) {
height: 16px;
padding: 0 var(--spacing-s); padding: 0 var(--spacing-s);
color: var(--spectrum-alias-text-color); color: var(--spectrum-alias-text-color);
} }
@ -319,6 +331,9 @@
background: var(--spectrum-global-color-gray-100) !important; background: var(--spectrum-global-color-gray-100) !important;
z-index: -2; z-index: -2;
} }
.code-editor :global(.cm-highlightSpace:before) {
color: var(--spectrum-global-color-gray-500);
}
/* Code selection */ /* Code selection */
.code-editor :global(.cm-selectionBackground) { .code-editor :global(.cm-selectionBackground) {

View File

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

View File

@ -1,5 +1,7 @@
<script> <script>
import { Input, Layout, Icon, Popover } from "@budibase/bbui" 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 addSnippet
export let snippets export let snippets
@ -15,16 +17,36 @@
{ {
name: "Square", name: "Square",
code: ` code: `
return function(num) { return function(num) {
return num * num return num * num
} }
`, `,
}, },
{ {
name: "HelloWorld", name: "HelloWorld",
code: ` 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:mouseenter={stopHidingPopover}
on:mouseleave={hidePopover} on:mouseleave={hidePopover}
> >
<!-- eslint-disable-next-line svelte/no-at-html-tags--> <div class="snippet-popover">
<pre class="snippet-popover">{@html hoveredSnippet.code}</pre> {#key hoveredSnippet}
<CodeEditor
value={hoveredSnippet.code.trim()}
mode={EditorModes.JS}
readonly
/>
{/key}
</div>
</Popover> </Popover>
<div class="snippet-side-panel"> <div class="snippet-side-panel">
@ -114,15 +143,13 @@
<div class="snippet-list"> <div class="snippet-list">
{#each filteredSnippets as snippet} {#each filteredSnippets as snippet}
<div class="snippet"> <div
<div class="snippet"
class="snippet-name" on:mouseenter={e => showSnippet(snippet, e.target)}
on:mouseenter={e => showSnippet(snippet, e.target)} on:mouseleave={hidePopover}
on:mouseleave={hidePopover} on:click={() => addSnippet(snippet)}
on:click={() => addSnippet(snippet)} >
> {snippet.name}
{snippet.name}
</div>
</div> </div>
{/each} {/each}
</div> </div>
@ -155,14 +182,18 @@
background: none; background: none;
padding: 0; padding: 0;
} }
.search-input { .search-input,
flex: 1; .title {
flex: 1 1 auto;
} }
/* List */ /* List */
.snippet-list { .snippet-list {
padding: 0 var(--spacing-l); padding: 0 var(--spacing-l);
padding-bottom: var(--spacing-l); padding-bottom: var(--spacing-l);
display: flex;
flex-direction: column;
gap: var(--spacing-s);
} }
.snippet { .snippet {
font-size: var(--font-size-s); font-size: var(--font-size-s);
@ -173,14 +204,14 @@
border-color 130ms ease-out; border-color 130ms ease-out;
word-wrap: break-word; word-wrap: break-word;
} }
.snippet:hover {
color: var(--spectrum-global-color-gray-900);
background-color: var(--spectrum-global-color-gray-50);
cursor: pointer;
}
/* Popover */ /* Popover */
.snippet-popover { .snippet-popover {
padding: 0; width: 400px;
margin: 0;
font-size: 12px;
white-space: pre;
text-overflow: ellipsis;
overflow: hidden;
} }
</style> </style>

View File

@ -38,4 +38,13 @@ export class BindingHelpers {
this.insertAtPos({ start, end, value: insertVal }) 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 return data
} }
const snippets = {
Square: `
return function(num) {
return num * num
}
`,
HelloWorld: `
return "Hello, world!"
`,
}
// Evaluates JS code against a certain context // Evaluates JS code against a certain context
module.exports.processJS = (handlebars, context) => { module.exports.processJS = (handlebars, context) => {
// for testing
context.snippets = snippets
if (!isJSAllowed() || (isBackendService() && !runJS)) { if (!isJSAllowed() || (isBackendService() && !runJS)) {
throw new Error("JS disabled in environment.") throw new Error("JS disabled in environment.")
} }