Improve logic around swapping binding panel tabs
This commit is contained in:
parent
65ca394f61
commit
5b3280832c
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue