@budibase/bbui 3.35.3 → 3.35.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bbui.mjs +2 -2
- package/dist/{easymde-B14-krJ4.mjs → easymde-eP6ldipj.mjs} +1 -1
- package/dist/{index-DpWvRzUi.mjs → index-BQfDByUb.mjs} +3461 -3288
- package/package.json +2 -2
- package/src/ActionButton/ActionButton.svelte +42 -7
- package/src/ButtonGroup/CollapsedButtonGroup.svelte +5 -1
- package/src/Form/Core/Signature.svelte +3 -2
- package/src/Form/Core/TextField.svelte +3 -3
- package/src/Form/Input.svelte +3 -3
- package/src/Form/Select.svelte +9 -4
- package/src/Markdown/MarkdownEditor.svelte +38 -9
- package/src/Markdown/SpectrumMDE.svelte +347 -2
- package/src/Modal/ModalContent.svelte +3 -3
- package/src/PhosphorIconPicker/PhosphorIconPicker.svelte +1 -1
- package/src/Table/Table.svelte +8 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/bbui",
|
|
3
3
|
"description": "A UI solution used in the different Budibase projects.",
|
|
4
|
-
"version": "3.35.
|
|
4
|
+
"version": "3.35.10",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"module": "dist/bbui.mjs",
|
|
7
7
|
"exports": {
|
|
@@ -103,5 +103,5 @@
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
},
|
|
106
|
-
"gitHead": "
|
|
106
|
+
"gitHead": "e2fa586beb21f730b22350e561d21ec1e2c5c2f0"
|
|
107
107
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import Icon from "../Icon/Icon.svelte"
|
|
5
5
|
import { fade } from "svelte/transition"
|
|
6
6
|
import { hexToRGBA } from "../helpers"
|
|
7
|
+
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
|
|
7
8
|
|
|
8
9
|
export let quiet: boolean = false
|
|
9
10
|
export let selected: boolean = false
|
|
@@ -15,10 +16,17 @@
|
|
|
15
16
|
export let noPadding: boolean = false
|
|
16
17
|
export let tooltip: string = ""
|
|
17
18
|
export let accentColor: string | null = null
|
|
19
|
+
export let loading: boolean = false
|
|
18
20
|
|
|
19
21
|
let showTooltip = false
|
|
20
22
|
|
|
21
23
|
$: accentStyle = getAccentStyle(accentColor)
|
|
24
|
+
$: isCustomIconSource =
|
|
25
|
+
!!icon &&
|
|
26
|
+
(icon.includes("/") ||
|
|
27
|
+
icon.startsWith("http://") ||
|
|
28
|
+
icon.startsWith("https://") ||
|
|
29
|
+
icon.startsWith("data:"))
|
|
22
30
|
|
|
23
31
|
const getAccentStyle = (color: string | null) => {
|
|
24
32
|
if (!color) {
|
|
@@ -52,14 +60,25 @@
|
|
|
52
60
|
{disabled}
|
|
53
61
|
style={accentStyle}
|
|
54
62
|
>
|
|
55
|
-
{#if icon}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
{#if icon && !loading}
|
|
64
|
+
{#if isCustomIconSource}
|
|
65
|
+
<img
|
|
66
|
+
class="custom-icon custom-icon-{size}"
|
|
67
|
+
src={icon}
|
|
68
|
+
alt=""
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
/>
|
|
71
|
+
{:else}
|
|
72
|
+
<Icon
|
|
73
|
+
name={icon}
|
|
74
|
+
{size}
|
|
75
|
+
color={`var(--spectrum-global-color-gray-${$$slots.default ? 600 : 700})`}
|
|
76
|
+
/>
|
|
77
|
+
{/if}
|
|
61
78
|
{/if}
|
|
62
|
-
{#if
|
|
79
|
+
{#if loading}
|
|
80
|
+
<ProgressCircle size="S" />
|
|
81
|
+
{:else if $$slots}
|
|
63
82
|
<span class="spectrum-ActionButton-label"><slot /></span>
|
|
64
83
|
{/if}
|
|
65
84
|
{#if tooltip && showTooltip}
|
|
@@ -143,6 +162,22 @@
|
|
|
143
162
|
.spectrum-ActionButton-label {
|
|
144
163
|
font-weight: 600;
|
|
145
164
|
}
|
|
165
|
+
.custom-icon {
|
|
166
|
+
object-fit: contain;
|
|
167
|
+
display: block;
|
|
168
|
+
}
|
|
169
|
+
.custom-icon-S {
|
|
170
|
+
width: 14px;
|
|
171
|
+
height: 14px;
|
|
172
|
+
}
|
|
173
|
+
.custom-icon-M {
|
|
174
|
+
width: 16px;
|
|
175
|
+
height: 16px;
|
|
176
|
+
}
|
|
177
|
+
.custom-icon-L {
|
|
178
|
+
width: 18px;
|
|
179
|
+
height: 18px;
|
|
180
|
+
}
|
|
146
181
|
.tooltip {
|
|
147
182
|
position: absolute;
|
|
148
183
|
pointer-events: none;
|
|
@@ -49,7 +49,11 @@
|
|
|
49
49
|
>
|
|
50
50
|
<Menu>
|
|
51
51
|
{#each buttons as button}
|
|
52
|
-
<MenuItem
|
|
52
|
+
<MenuItem
|
|
53
|
+
icon={button.icon}
|
|
54
|
+
on:click={() => handleClick(button)}
|
|
55
|
+
disabled={button.disabled}
|
|
56
|
+
>
|
|
53
57
|
{button.text || "Button"}
|
|
54
58
|
</MenuItem>
|
|
55
59
|
{/each}
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
signature.weight = 4
|
|
92
92
|
signature.smoothing = 2
|
|
93
93
|
|
|
94
|
-
canvasWrap.style.width =
|
|
95
|
-
canvasWrap.style.
|
|
94
|
+
canvasWrap.style.width = "100%"
|
|
95
|
+
canvasWrap.style.maxWidth = `${width}px`
|
|
96
96
|
|
|
97
97
|
const { width: wrapWidth, height: wrapHeight } =
|
|
98
98
|
canvasWrap.getBoundingClientRect()
|
|
@@ -208,6 +208,7 @@
|
|
|
208
208
|
}
|
|
209
209
|
#signature-canvas {
|
|
210
210
|
max-width: var(--max-sig-width);
|
|
211
|
+
width: 100%;
|
|
211
212
|
max-height: var(--max-sig-height);
|
|
212
213
|
}
|
|
213
214
|
.signature.light img,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="V extends string | number">
|
|
2
2
|
import "@spectrum-css/textfield/dist/index-vars.css"
|
|
3
3
|
import { createEventDispatcher, onMount, tick } from "svelte"
|
|
4
4
|
import type { FullAutoFill } from "svelte/elements"
|
|
5
5
|
import type { UIEvent } from "@budibase/types"
|
|
6
6
|
|
|
7
|
-
export let value:
|
|
7
|
+
export let value: V | null = null
|
|
8
8
|
export let placeholder: string | undefined = undefined
|
|
9
9
|
export let type = "text"
|
|
10
10
|
export let disabled = false
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
export let autofocus: boolean | null = false
|
|
17
17
|
export let autocomplete: FullAutoFill | boolean | null | undefined = undefined
|
|
18
18
|
|
|
19
|
-
const dispatch = createEventDispatcher()
|
|
19
|
+
const dispatch = createEventDispatcher<{ change: V | null }>()
|
|
20
20
|
|
|
21
21
|
let field: any
|
|
22
22
|
let focus = false
|
package/src/Form/Input.svelte
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="V extends string | number">
|
|
2
2
|
import Field from "./Field.svelte"
|
|
3
3
|
import TextField from "./Core/TextField.svelte"
|
|
4
4
|
import { createEventDispatcher } from "svelte"
|
|
5
5
|
import type { FullAutoFill } from "svelte/elements"
|
|
6
6
|
|
|
7
|
-
export let value:
|
|
7
|
+
export let value: V | null | undefined = undefined
|
|
8
8
|
export let label: string | undefined = undefined
|
|
9
9
|
export let labelPosition: "above" | "left" = "above"
|
|
10
10
|
export let placeholder: string | undefined = undefined
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
export let description: string | undefined = undefined
|
|
22
22
|
|
|
23
23
|
const dispatch = createEventDispatcher<{
|
|
24
|
-
change:
|
|
24
|
+
change: V
|
|
25
25
|
enterkey: KeyboardEvent
|
|
26
26
|
blur: any
|
|
27
27
|
}>()
|
package/src/Form/Select.svelte
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
<script
|
|
1
|
+
<script
|
|
2
|
+
lang="ts"
|
|
3
|
+
generics="O extends any, V, P extends string | boolean = string | boolean"
|
|
4
|
+
>
|
|
2
5
|
import Field from "./Field.svelte"
|
|
3
6
|
import Select from "./Core/Select.svelte"
|
|
4
7
|
import { createEventDispatcher } from "svelte"
|
|
@@ -11,7 +14,7 @@
|
|
|
11
14
|
export let readonly: boolean = false
|
|
12
15
|
export let labelPosition: LabelPosition = "above"
|
|
13
16
|
export let error: string | undefined = undefined
|
|
14
|
-
export let placeholder:
|
|
17
|
+
export let placeholder: P = "Choose an option" as P
|
|
15
18
|
export let options: O[] = []
|
|
16
19
|
export let getOptionLabel = (option: O, _index?: number) =>
|
|
17
20
|
extractProperty(option, "label")
|
|
@@ -53,8 +56,10 @@
|
|
|
53
56
|
export let wrapText: boolean = false
|
|
54
57
|
export let description: string | undefined = undefined
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
type ChangeValue = P extends false | "" ? V : V | undefined
|
|
60
|
+
|
|
61
|
+
const dispatch = createEventDispatcher<{ change: ChangeValue }>()
|
|
62
|
+
const onChange = (e: CustomEvent<ChangeValue>) => {
|
|
58
63
|
value = e.detail
|
|
59
64
|
dispatch("change", e.detail)
|
|
60
65
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type EasyMDE from "easymde"
|
|
3
|
+
import { onDestroy } from "svelte"
|
|
2
4
|
import SpectrumMDE from "./SpectrumMDE.svelte"
|
|
3
5
|
import { createEventDispatcher } from "svelte"
|
|
4
6
|
|
|
@@ -14,26 +16,53 @@
|
|
|
14
16
|
const dispatch = createEventDispatcher()
|
|
15
17
|
|
|
16
18
|
let latestValue: string | null
|
|
17
|
-
|
|
19
|
+
interface EditorInstance extends EasyMDE {
|
|
20
|
+
togglePreview: () => void
|
|
21
|
+
value: (_value?: string) => string
|
|
22
|
+
}
|
|
23
|
+
let mde: EditorInstance | null = null
|
|
24
|
+
let blurBoundTo: EditorInstance | null = null
|
|
25
|
+
|
|
26
|
+
const bindBlurHandler = (editor: EditorInstance) => {
|
|
27
|
+
if (blurBoundTo === editor) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
if (blurBoundTo) {
|
|
31
|
+
blurBoundTo.codemirror.off("blur", update)
|
|
32
|
+
}
|
|
33
|
+
editor.codemirror.on("blur", update)
|
|
34
|
+
blurBoundTo = editor
|
|
35
|
+
}
|
|
18
36
|
|
|
19
37
|
// Ensure the value is updated if the value prop changes outside the editor's
|
|
20
38
|
// control
|
|
21
|
-
$: checkValue(value)
|
|
22
|
-
$: mde
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
$: checkValue(mde, value)
|
|
40
|
+
$: if (mde) {
|
|
41
|
+
bindBlurHandler(mde)
|
|
42
|
+
}
|
|
43
|
+
$: if ((readonly || disabled) && mde) {
|
|
44
|
+
mde.togglePreview?.()
|
|
25
45
|
}
|
|
26
46
|
|
|
27
|
-
const checkValue = (val: string | null) => {
|
|
28
|
-
if (
|
|
29
|
-
|
|
47
|
+
const checkValue = (editor: EditorInstance | null, val: string | null) => {
|
|
48
|
+
if (editor && val !== latestValue) {
|
|
49
|
+
editor.value(val ?? "")
|
|
30
50
|
}
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
const update = () => {
|
|
54
|
+
if (!mde) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
34
57
|
latestValue = mde.value()
|
|
35
58
|
dispatch("change", latestValue)
|
|
36
59
|
}
|
|
60
|
+
|
|
61
|
+
onDestroy(() => {
|
|
62
|
+
if (blurBoundTo) {
|
|
63
|
+
blurBoundTo.codemirror.off("blur", update)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
37
66
|
</script>
|
|
38
67
|
|
|
39
68
|
{#key height}
|
|
@@ -47,8 +76,8 @@
|
|
|
47
76
|
easyMDEOptions={{
|
|
48
77
|
initialValue: value,
|
|
49
78
|
placeholder,
|
|
50
|
-
toolbar: disabled || readonly ? false : undefined,
|
|
51
79
|
...easyMDEOptions,
|
|
80
|
+
toolbar: disabled || readonly ? false : easyMDEOptions?.toolbar,
|
|
52
81
|
}}
|
|
53
82
|
/>
|
|
54
83
|
{/key}
|
|
@@ -1,17 +1,322 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type EasyMDE from "easymde"
|
|
3
3
|
import "easymde/dist/easymde.min.css"
|
|
4
|
-
import { onDestroy, onMount } from "svelte"
|
|
4
|
+
import { onDestroy, onMount, tick } from "svelte"
|
|
5
|
+
import ColorPicker from "../ColorPicker/ColorPicker.svelte"
|
|
5
6
|
|
|
6
7
|
export let height: string | null = null
|
|
7
8
|
export let scroll: boolean = true
|
|
8
9
|
export let easyMDEOptions: Record<string, any> | null = null
|
|
9
|
-
|
|
10
|
+
interface EditorInstance extends EasyMDE {
|
|
11
|
+
toolbarElements?: Record<string, HTMLElement>
|
|
12
|
+
}
|
|
13
|
+
interface EditorSelectionRange {
|
|
14
|
+
anchor: { line: number; ch: number }
|
|
15
|
+
head: { line: number; ch: number }
|
|
16
|
+
}
|
|
17
|
+
type ColorMode = "text" | "highlight"
|
|
18
|
+
type ActiveColors = Record<ColorMode, string | null>
|
|
19
|
+
|
|
20
|
+
export let mde: EditorInstance | null = null
|
|
10
21
|
export let id: string | null = null
|
|
11
22
|
export let fullScreenOffset: { x: string; y: string } | null = null
|
|
12
23
|
export let disabled: boolean = false
|
|
13
24
|
|
|
14
25
|
let element: HTMLTextAreaElement | undefined = undefined
|
|
26
|
+
let colorPickerAnchor: HTMLDivElement | undefined = undefined
|
|
27
|
+
let colorPickerX = 0
|
|
28
|
+
let colorPickerY = 0
|
|
29
|
+
let colorPickerKey = 0
|
|
30
|
+
let activeMode: ColorMode = "text"
|
|
31
|
+
let selectedColors: Record<ColorMode, string> = {
|
|
32
|
+
text: "#d73f09",
|
|
33
|
+
highlight: "#fff59d",
|
|
34
|
+
}
|
|
35
|
+
let pendingSelections: EditorSelectionRange[] | null = null
|
|
36
|
+
let lastNonEmptySelections: EditorSelectionRange[] | null = null
|
|
37
|
+
let cursorBoundTo: EditorInstance | null = null
|
|
38
|
+
|
|
39
|
+
const modeConfig = {
|
|
40
|
+
text: {
|
|
41
|
+
toolbarButtonName: "text-color",
|
|
42
|
+
wrapperTag: "span",
|
|
43
|
+
stylePrefix: "color",
|
|
44
|
+
className: "fa fa-font",
|
|
45
|
+
title: "Text Color",
|
|
46
|
+
},
|
|
47
|
+
highlight: {
|
|
48
|
+
toolbarButtonName: "text-highlight",
|
|
49
|
+
wrapperTag: "mark",
|
|
50
|
+
stylePrefix: "background-color",
|
|
51
|
+
className: "fa fa-paint-brush",
|
|
52
|
+
title: "Highlight Color",
|
|
53
|
+
},
|
|
54
|
+
} as const
|
|
55
|
+
|
|
56
|
+
const cloneSelections = (selections: EditorSelectionRange[]) =>
|
|
57
|
+
selections.map(selection => ({
|
|
58
|
+
anchor: { ...selection.anchor },
|
|
59
|
+
head: { ...selection.head },
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
const hasSelectedText = (editor: EditorInstance) =>
|
|
63
|
+
editor.codemirror.getSelections().some((text: string) => text.length > 0)
|
|
64
|
+
|
|
65
|
+
const getSelections = (editor: EditorInstance) =>
|
|
66
|
+
cloneSelections(
|
|
67
|
+
editor.codemirror.listSelections() as EditorSelectionRange[]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const getValidatedColor = (color: string, mode: ColorMode) => {
|
|
71
|
+
const trimmedColor = color.trim()
|
|
72
|
+
if (!trimmedColor) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cssProperty = modeConfig[mode].stylePrefix
|
|
77
|
+
if (typeof CSS === "undefined" || typeof CSS.supports !== "function") {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
if (!CSS.supports(cssProperty, trimmedColor)) {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
return trimmedColor
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const getColorFromTag = (tagText: string, mode: ColorMode) => {
|
|
87
|
+
const styleRegex = new RegExp(
|
|
88
|
+
`${modeConfig[mode].stylePrefix}\\s*:\\s*([^;"']+)`,
|
|
89
|
+
"i"
|
|
90
|
+
)
|
|
91
|
+
const styleMatch = tagText.match(styleRegex)
|
|
92
|
+
if (!styleMatch?.[1]) {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
return getValidatedColor(styleMatch[1], mode)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const getActiveColorsBeforeCursor = (
|
|
99
|
+
textBeforeCursor: string
|
|
100
|
+
): ActiveColors => {
|
|
101
|
+
const stacks: Record<ColorMode, string[]> = {
|
|
102
|
+
text: [],
|
|
103
|
+
highlight: [],
|
|
104
|
+
}
|
|
105
|
+
const tokenRegex = /<(\/?)(span|mark)\b[^>]*>/gi
|
|
106
|
+
for (const token of textBeforeCursor.matchAll(tokenRegex)) {
|
|
107
|
+
const tokenValue = token[0]
|
|
108
|
+
const isClosing = token[1] === "/"
|
|
109
|
+
const mode: ColorMode =
|
|
110
|
+
token[2].toLowerCase() === "mark" ? "highlight" : "text"
|
|
111
|
+
if (isClosing) {
|
|
112
|
+
stacks[mode].pop()
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
const color = getColorFromTag(tokenValue, mode)
|
|
116
|
+
if (color) {
|
|
117
|
+
stacks[mode].push(color)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
text: stacks.text.length ? stacks.text[stacks.text.length - 1] : null,
|
|
122
|
+
highlight: stacks.highlight.length
|
|
123
|
+
? stacks.highlight[stacks.highlight.length - 1]
|
|
124
|
+
: null,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const getColorsAtCursor = (
|
|
129
|
+
content: string,
|
|
130
|
+
cursorIndex: number
|
|
131
|
+
): ActiveColors => {
|
|
132
|
+
const result: ActiveColors = {
|
|
133
|
+
text: null,
|
|
134
|
+
highlight: null,
|
|
135
|
+
}
|
|
136
|
+
const openIndex = content.lastIndexOf("<", cursorIndex)
|
|
137
|
+
if (openIndex < 0) {
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
const closeBeforeIndex = content.lastIndexOf(">", cursorIndex - 1)
|
|
141
|
+
if (closeBeforeIndex > openIndex) {
|
|
142
|
+
return result
|
|
143
|
+
}
|
|
144
|
+
const closeAfterIndex = content.indexOf(">", openIndex)
|
|
145
|
+
if (closeAfterIndex < 0) {
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
const tagText = content.slice(openIndex, closeAfterIndex + 1)
|
|
149
|
+
if (/^<span\b/i.test(tagText)) {
|
|
150
|
+
result.text = getColorFromTag(tagText, "text")
|
|
151
|
+
}
|
|
152
|
+
if (/^<mark\b/i.test(tagText)) {
|
|
153
|
+
result.highlight = getColorFromTag(tagText, "highlight")
|
|
154
|
+
}
|
|
155
|
+
return result
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const setToolbarIconColor = (
|
|
159
|
+
editor: EditorInstance,
|
|
160
|
+
mode: ColorMode,
|
|
161
|
+
color: string | null
|
|
162
|
+
) => {
|
|
163
|
+
const button = editor.toolbarElements?.[modeConfig[mode].toolbarButtonName]
|
|
164
|
+
const icon = button?.querySelector("i") as HTMLElement | null
|
|
165
|
+
if (!icon) {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
if (!color) {
|
|
169
|
+
icon.style.removeProperty("color")
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
icon.style.color = color
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const syncToolbarIconColors = (editor: EditorInstance) => {
|
|
176
|
+
const cursor = editor.codemirror.getCursor("head")
|
|
177
|
+
const cursorIndex = editor.codemirror.indexFromPos(cursor)
|
|
178
|
+
const content = editor.codemirror.getValue()
|
|
179
|
+
const contentBeforeCursor = content.slice(0, cursorIndex)
|
|
180
|
+
const activeColors = getActiveColorsBeforeCursor(contentBeforeCursor)
|
|
181
|
+
const cursorColors = getColorsAtCursor(content, cursorIndex)
|
|
182
|
+
const textColor = cursorColors.text || activeColors.text
|
|
183
|
+
const highlightColor = cursorColors.highlight || activeColors.highlight
|
|
184
|
+
setToolbarIconColor(editor, "text", textColor)
|
|
185
|
+
setToolbarIconColor(editor, "highlight", highlightColor)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const cacheSelection = () => {
|
|
189
|
+
if (!mde || !hasSelectedText(mde)) {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
lastNonEmptySelections = getSelections(mde)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const onCursorActivity = () => {
|
|
196
|
+
if (!mde) {
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
cacheSelection()
|
|
200
|
+
syncToolbarIconColors(mde)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const bindCursorListener = (editor: EditorInstance) => {
|
|
204
|
+
if (cursorBoundTo === editor) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
if (cursorBoundTo) {
|
|
208
|
+
cursorBoundTo.codemirror.off("cursorActivity", onCursorActivity)
|
|
209
|
+
}
|
|
210
|
+
editor.codemirror.on("cursorActivity", onCursorActivity)
|
|
211
|
+
cursorBoundTo = editor
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const openColorPicker = async (editor: EasyMDE, mode: ColorMode) => {
|
|
215
|
+
const typedEditor = editor as EditorInstance
|
|
216
|
+
const currentSelections = getSelections(typedEditor)
|
|
217
|
+
const activeSelections = hasSelectedText(typedEditor)
|
|
218
|
+
? currentSelections
|
|
219
|
+
: lastNonEmptySelections || currentSelections
|
|
220
|
+
pendingSelections = cloneSelections(activeSelections)
|
|
221
|
+
activeMode = mode
|
|
222
|
+
const toolbarButtonName = modeConfig[mode].toolbarButtonName
|
|
223
|
+
const toolbarButton = typedEditor.toolbarElements?.[toolbarButtonName]
|
|
224
|
+
if (toolbarButton) {
|
|
225
|
+
const rect = toolbarButton.getBoundingClientRect()
|
|
226
|
+
colorPickerX = Math.round(rect.left + rect.width / 2)
|
|
227
|
+
colorPickerY = Math.round(rect.bottom)
|
|
228
|
+
}
|
|
229
|
+
colorPickerKey += 1
|
|
230
|
+
await tick()
|
|
231
|
+
const trigger = colorPickerAnchor?.querySelector(".preview") as
|
|
232
|
+
| HTMLElement
|
|
233
|
+
| undefined
|
|
234
|
+
trigger?.click()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const applyStyledSelections = (color: string, mode: ColorMode) => {
|
|
238
|
+
if (!pendingSelections || !mde) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
const safeColor = getValidatedColor(color, mode)
|
|
242
|
+
if (!safeColor) {
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
mde.codemirror.focus()
|
|
246
|
+
mde.codemirror.setSelections(pendingSelections)
|
|
247
|
+
const selectedTexts = mde.codemirror.getSelections()
|
|
248
|
+
const hasText = selectedTexts?.some((text: string) => text.length > 0)
|
|
249
|
+
if (!selectedTexts?.length || !hasText) {
|
|
250
|
+
pendingSelections = null
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
const { wrapperTag, stylePrefix } = modeConfig[mode]
|
|
254
|
+
const styleAttr = `${stylePrefix}: ${safeColor};`
|
|
255
|
+
const replacements = selectedTexts.map(
|
|
256
|
+
(text: string) =>
|
|
257
|
+
`<${wrapperTag} style="${styleAttr}">${text}</${wrapperTag}>`
|
|
258
|
+
)
|
|
259
|
+
mde.codemirror.replaceSelections(replacements)
|
|
260
|
+
pendingSelections = null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const onColorChange = (event: CustomEvent<string | undefined>) => {
|
|
264
|
+
const color = event.detail?.trim()
|
|
265
|
+
if (!color) {
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
selectedColors = {
|
|
269
|
+
...selectedColors,
|
|
270
|
+
[activeMode]: color,
|
|
271
|
+
}
|
|
272
|
+
applyStyledSelections(color, activeMode)
|
|
273
|
+
if (mde) {
|
|
274
|
+
syncToolbarIconColors(mde)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const textColorToolbarButton = {
|
|
279
|
+
name: modeConfig.text.toolbarButtonName,
|
|
280
|
+
action: (editor: EasyMDE) => openColorPicker(editor, "text"),
|
|
281
|
+
className: modeConfig.text.className,
|
|
282
|
+
title: modeConfig.text.title,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const highlightToolbarButton = {
|
|
286
|
+
name: modeConfig.highlight.toolbarButtonName,
|
|
287
|
+
action: (editor: EasyMDE) => openColorPicker(editor, "highlight"),
|
|
288
|
+
className: modeConfig.highlight.className,
|
|
289
|
+
title: modeConfig.highlight.title,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const defaultToolbar = [
|
|
293
|
+
"bold",
|
|
294
|
+
"italic",
|
|
295
|
+
"heading",
|
|
296
|
+
"|",
|
|
297
|
+
"quote",
|
|
298
|
+
"unordered-list",
|
|
299
|
+
"ordered-list",
|
|
300
|
+
"|",
|
|
301
|
+
"link",
|
|
302
|
+
"image",
|
|
303
|
+
"|",
|
|
304
|
+
textColorToolbarButton,
|
|
305
|
+
highlightToolbarButton,
|
|
306
|
+
"|",
|
|
307
|
+
"preview",
|
|
308
|
+
"side-by-side",
|
|
309
|
+
"fullscreen",
|
|
310
|
+
"|",
|
|
311
|
+
"guide",
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
const getToolbar = (options: Record<string, any> | null) => {
|
|
315
|
+
if (options?.toolbar !== undefined) {
|
|
316
|
+
return options.toolbar
|
|
317
|
+
}
|
|
318
|
+
return defaultToolbar
|
|
319
|
+
}
|
|
15
320
|
|
|
16
321
|
onMount(async () => {
|
|
17
322
|
height = height || "200px"
|
|
@@ -24,10 +329,16 @@
|
|
|
24
329
|
maxHeight: scroll ? height : undefined,
|
|
25
330
|
minHeight: scroll ? undefined : height,
|
|
26
331
|
...easyMDEOptions,
|
|
332
|
+
toolbar: getToolbar(easyMDEOptions),
|
|
27
333
|
})
|
|
334
|
+
bindCursorListener(mde)
|
|
335
|
+
syncToolbarIconColors(mde)
|
|
28
336
|
})
|
|
29
337
|
|
|
30
338
|
onDestroy(() => {
|
|
339
|
+
if (cursorBoundTo) {
|
|
340
|
+
cursorBoundTo.codemirror.off("cursorActivity", onCursorActivity)
|
|
341
|
+
}
|
|
31
342
|
mde?.toTextArea()
|
|
32
343
|
})
|
|
33
344
|
|
|
@@ -45,7 +356,30 @@
|
|
|
45
356
|
<textarea disabled {id} bind:this={element}></textarea>
|
|
46
357
|
</div>
|
|
47
358
|
|
|
359
|
+
<div
|
|
360
|
+
bind:this={colorPickerAnchor}
|
|
361
|
+
class="budibase-color-picker-anchor"
|
|
362
|
+
style={`left:${colorPickerX}px;top:${colorPickerY}px;`}
|
|
363
|
+
>
|
|
364
|
+
{#key colorPickerKey}
|
|
365
|
+
<ColorPicker
|
|
366
|
+
value={selectedColors[activeMode]}
|
|
367
|
+
size="S"
|
|
368
|
+
on:change={onColorChange}
|
|
369
|
+
/>
|
|
370
|
+
{/key}
|
|
371
|
+
</div>
|
|
372
|
+
|
|
48
373
|
<style>
|
|
374
|
+
.budibase-color-picker-anchor {
|
|
375
|
+
position: fixed;
|
|
376
|
+
width: 1px;
|
|
377
|
+
height: 1px;
|
|
378
|
+
overflow: hidden;
|
|
379
|
+
opacity: 0;
|
|
380
|
+
pointer-events: none;
|
|
381
|
+
}
|
|
382
|
+
|
|
49
383
|
/* Disabled styles */
|
|
50
384
|
.disabled :global(textarea) {
|
|
51
385
|
display: none;
|
|
@@ -114,6 +448,17 @@
|
|
|
114
448
|
:global(.EasyMDEContainer .cm-s-easymde .cm-url) {
|
|
115
449
|
color: var(--spectrum-global-color-gray-500);
|
|
116
450
|
}
|
|
451
|
+
/* Keep inline HTML markup readable across themes */
|
|
452
|
+
:global(.EasyMDEContainer .cm-s-easymde .cm-tag) {
|
|
453
|
+
color: var(--spectrum-alias-text-color);
|
|
454
|
+
opacity: 0.9;
|
|
455
|
+
}
|
|
456
|
+
:global(.EasyMDEContainer .cm-s-easymde .cm-attribute) {
|
|
457
|
+
color: var(--spectrum-global-color-gray-700);
|
|
458
|
+
}
|
|
459
|
+
:global(.EasyMDEContainer .cm-s-easymde .cm-string) {
|
|
460
|
+
color: var(--spectrum-global-color-gray-600);
|
|
461
|
+
}
|
|
117
462
|
/* Markdown italics in the editor */
|
|
118
463
|
:global(.EasyMDEContainer .cm-s-easymde .cm-em) {
|
|
119
464
|
font-style: italic !important;
|