@aleph-alpha/chat-kit 6.4.1 → 6.4.3
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/{CkHistory.vue_vue_type_script_setup_true_lang-BvRIp3m6.js → CkHistory.vue_vue_type_script_setup_true_lang-Bv3FuKT4.js} +85 -35
- package/dist/chat-kit.css +63 -19
- package/dist/components/base/CkCodeEditor/CkCodeEditor.stories.d.ts.map +1 -1
- package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts +2 -0
- package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts.map +1 -1
- package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts +5 -1
- package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts.map +1 -1
- package/dist/components/base/CkCodeEditor/index.d.ts +1 -0
- package/dist/components/base/CkCodeEditor/index.d.ts.map +1 -1
- package/dist/components/base/CkCodeEditor/loadCodeMirror.d.ts +17 -0
- package/dist/components/base/CkCodeEditor/loadCodeMirror.d.ts.map +1 -0
- package/dist/components/base/CkCodeEditor/types.d.ts +25 -6
- package/dist/components/base/CkCodeEditor/types.d.ts.map +1 -1
- package/dist/components/base/index.d.ts +2 -2
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -2
- package/dist/{createCodeMirror-BO-TWTzn.js → createCodeMirror-CN8xL8qU.js} +20 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/package.json +2 -2
- package/src/components/base/CkCodeEditor/CkCodeEditor.spec.ts +99 -0
- package/src/components/base/CkCodeEditor/CkCodeEditor.stories.ts +10 -0
- package/src/components/base/CkCodeEditor/CkCodeEditor.vue +123 -34
- package/src/components/base/CkCodeEditor/createCodeMirror.ts +34 -13
- package/src/components/base/CkCodeEditor/index.ts +1 -0
- package/src/components/base/CkCodeEditor/loadCodeMirror.ts +28 -0
- package/src/components/base/CkCodeEditor/types.ts +30 -6
- package/src/components/base/index.ts +2 -1
- package/src/components/index.ts +2 -0
- package/src/index.ts +2 -0
|
@@ -114,22 +114,22 @@ function baseExtensions(options) {
|
|
|
114
114
|
...options.placeholder ? [placeholder(options.placeholder)] : []
|
|
115
115
|
];
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
function createCodeMirror(options) {
|
|
118
118
|
const languageSlot = new Compartment();
|
|
119
119
|
const readonlySlot = new Compartment();
|
|
120
120
|
const lineNumbersSlot = new Compartment();
|
|
121
121
|
const wrappingSlot = new Compartment();
|
|
122
122
|
const badgeSlot = new Compartment();
|
|
123
123
|
const lineSeparatorSlot = new Compartment();
|
|
124
|
-
const support = await resolveLanguage(options.language);
|
|
125
124
|
let languageRequestId = 0;
|
|
125
|
+
let destroyed = false;
|
|
126
126
|
const view = new EditorView({
|
|
127
127
|
parent: options.parent,
|
|
128
128
|
state: EditorState.create({
|
|
129
129
|
doc: options.doc,
|
|
130
130
|
extensions: [
|
|
131
131
|
...baseExtensions(options),
|
|
132
|
-
languageSlot.of(
|
|
132
|
+
languageSlot.of([]),
|
|
133
133
|
readonlySlot.of(readonlyState(options.readonly)),
|
|
134
134
|
lineNumbersSlot.of(options.showLineNumbers ? lineNumbers() : []),
|
|
135
135
|
wrappingSlot.of(options.lineWrapping ? EditorView.lineWrapping : []),
|
|
@@ -141,8 +141,20 @@ async function createCodeMirror(options) {
|
|
|
141
141
|
})
|
|
142
142
|
});
|
|
143
143
|
function reconfigure(slot, extension) {
|
|
144
|
+
if (destroyed) return;
|
|
144
145
|
view.dispatch({ effects: slot.reconfigure(extension) });
|
|
145
146
|
}
|
|
147
|
+
async function setLanguage(language) {
|
|
148
|
+
const request = ++languageRequestId;
|
|
149
|
+
const next = await resolveLanguage(language);
|
|
150
|
+
if (request !== languageRequestId) return;
|
|
151
|
+
try {
|
|
152
|
+
reconfigure(languageSlot, next ? [next] : []);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.warn(`[CkCodeEditor] Could not apply "${language}":`, error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
setLanguage(options.language).catch(() => void 0);
|
|
146
158
|
return {
|
|
147
159
|
view,
|
|
148
160
|
getCode: () => readDoc(view.state),
|
|
@@ -158,12 +170,7 @@ async function createCodeMirror(options) {
|
|
|
158
170
|
selection: { anchor: cursor }
|
|
159
171
|
});
|
|
160
172
|
},
|
|
161
|
-
setLanguage
|
|
162
|
-
const request = ++languageRequestId;
|
|
163
|
-
const next = await resolveLanguage(language);
|
|
164
|
-
if (request !== languageRequestId) return;
|
|
165
|
-
reconfigure(languageSlot, next ? [next] : []);
|
|
166
|
-
},
|
|
173
|
+
setLanguage,
|
|
167
174
|
setReadonly: (readonly) => {
|
|
168
175
|
reconfigure(readonlySlot, readonlyState(readonly));
|
|
169
176
|
},
|
|
@@ -177,7 +184,10 @@ async function createCodeMirror(options) {
|
|
|
177
184
|
reconfigure(badgeSlot, rule ? badgeExtension(rule) : []);
|
|
178
185
|
},
|
|
179
186
|
focus: () => view.focus(),
|
|
180
|
-
destroy: () =>
|
|
187
|
+
destroy: () => {
|
|
188
|
+
destroyed = true;
|
|
189
|
+
view.destroy();
|
|
190
|
+
}
|
|
181
191
|
};
|
|
182
192
|
}
|
|
183
193
|
export {
|
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ export { useChat, useConversationSessions, useConversations, } from './composabl
|
|
|
6
6
|
export type { ChatActions, ChatState, ConversationSession, CreateConversationInput, MessageHandler, MutationOptions, UseChatReturn, UseConversationSessionsReturn, UseConversationsOptions, UseConversationsReturn, } from './composables';
|
|
7
7
|
export { SdkError, createDocumentUploadProvider, defaultDocumentUploadConfig, defaultEndpoints, useDocumentUploadWithDefaultConfig, } from './composables/fileUpload';
|
|
8
8
|
export type { DocumentUploadConfig, DocumentUploadProvider, FileEntry, FileStatus, } from './composables/fileUpload';
|
|
9
|
-
export { CkAgentMessage, CkAgentMessageHeader, CkHeaderActivityButton, CkCitation, CkCitationLabel, CkCitationCard, CkConversation, CkConversationLayout, CkCopyMessageButton, CkDropZoneOverlay, CkFeedbackButton, CkFeedbackDialog, CkFileItemsList, CkHistory, CkHistoryItem, CkHistoryWrapper, CkMarkdownRenderer, CkMessageButton, CkModelSelector, CkPromptSendButton, CkPromptMenuButton, CkPromptStopButton, CkPromptInput, CkPromptInputDisclaimer, CkRegenerateMessageButton, CkSendIcon, CkStopIcon, CkUserMessage, CkEditableUserMessage, CkMilkdownEditor, CkCodeEditor, } from './components';
|
|
10
|
-
export type { CkAgentMessageHeaderProps, CkHeaderActivityButtonProps, CkHeaderActivityButtonEmits, CkCitationProps, CkCitationEmits, CkCitationSource, CkCitationLabelProps, CkCitationCardProps, CkCitationCardEmits, CkConversationLayoutProps, CkConversationProps, CkDropZoneOverlayProps, CkDropZoneOverlayEmits, CkDropZoneRejection, CkDropZoneRejectionReason, CkFeedbackButtonProps, CkFeedbackDialogEmits, CkFeedbackDialogProps, CkFeedbackDialogSubmitPayload, CkFeedbackReason, CkFeedbackSentiment, CkFeedbackValue, CkFileEntry, CkFileItemsListEmits, CkFileItemsListProps, CkHistoryEmits, CkHistoryItemEmits, CkHistoryItemProps, CkHistoryProps, CkHistoryWrapperProps, CkMarkdownElementClasses, CkMarkdownRendererProps, CkMarkdownRendererEmits, CkModelOption, CkModelSelectorProps, CkModelSelectorEmits, CkPromptMenuButtonProps, CkUserMessageProps, CkEditableUserMessageProps, CkEditableUserMessageEmits, CkMilkdownEditorEmits, CkMilkdownEditorExposed, CkMilkdownEditorProps, CkCodeEditorEmits, CkCodeEditorExposed, CkCodeEditorProps, } from './components';
|
|
9
|
+
export { CkAgentMessage, CkAgentMessageHeader, CkHeaderActivityButton, CkCitation, CkCitationLabel, CkCitationCard, CkConversation, CkConversationLayout, CkCopyMessageButton, CkDropZoneOverlay, CkFeedbackButton, CkFeedbackDialog, CkFileItemsList, CkHistory, CkHistoryItem, CkHistoryWrapper, CkMarkdownRenderer, CkMessageButton, CkModelSelector, CkPromptSendButton, CkPromptMenuButton, CkPromptStopButton, CkPromptInput, CkPromptInputDisclaimer, CkRegenerateMessageButton, CkSendIcon, CkStopIcon, CkUserMessage, CkEditableUserMessage, CkMilkdownEditor, CkCodeEditor, preloadCkCodeEditor, } from './components';
|
|
10
|
+
export type { CkAgentMessageHeaderProps, CkHeaderActivityButtonProps, CkHeaderActivityButtonEmits, CkCitationProps, CkCitationEmits, CkCitationSource, CkCitationLabelProps, CkCitationCardProps, CkCitationCardEmits, CkConversationLayoutProps, CkConversationProps, CkDropZoneOverlayProps, CkDropZoneOverlayEmits, CkDropZoneRejection, CkDropZoneRejectionReason, CkFeedbackButtonProps, CkFeedbackDialogEmits, CkFeedbackDialogProps, CkFeedbackDialogSubmitPayload, CkFeedbackReason, CkFeedbackSentiment, CkFeedbackValue, CkFileEntry, CkFileItemsListEmits, CkFileItemsListProps, CkHistoryEmits, CkHistoryItemEmits, CkHistoryItemProps, CkHistoryProps, CkHistoryWrapperProps, CkMarkdownElementClasses, CkMarkdownRendererProps, CkMarkdownRendererEmits, CkModelOption, CkModelSelectorProps, CkModelSelectorEmits, CkPromptMenuButtonProps, CkUserMessageProps, CkEditableUserMessageProps, CkEditableUserMessageEmits, CkMilkdownEditorEmits, CkMilkdownEditorExposed, CkMilkdownEditorProps, CkCodeEditorEmits, CkCodeEditorExposed, CkCodeEditorFadeSurface, CkCodeEditorProps, } from './components';
|
|
11
11
|
export { formatRelativeTime, markdownToPlainText } from './helpers';
|
|
12
12
|
export type { ActiveComparison, BaseMessage, ComparisonLane, Conversation, ConversationTree, MessageContent, MessageContentStatus, MessageMetadata, MessageReasoningContent, MessageRole, MessageStatus, MessageTextContent, MessageToolContent, TreeMessage, } from './types';
|
|
13
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,iBAAiB,CAAC;AAGzB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AASpB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EACL,OAAO,EACP,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,WAAW,EACX,SAAS,EACT,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,aAAa,EACb,6BAA6B,EAC7B,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,QAAQ,EACR,4BAA4B,EAC5B,2BAA2B,EAC3B,gBAAgB,EAChB,kCAAkC,GACnC,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,oBAAoB,EACpB,sBAAsB,EACtB,SAAS,EACT,UAAU,GACX,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,uBAAuB,EACvB,yBAAyB,EACzB,UAAU,EACV,UAAU,EACV,aAAa,EACb,qBAAqB,EACrB,gBAAgB,EAChB,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,iBAAiB,CAAC;AAGzB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AASpB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EACL,OAAO,EACP,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,WAAW,EACX,SAAS,EACT,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,aAAa,EACb,6BAA6B,EAC7B,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,QAAQ,EACR,4BAA4B,EAC5B,2BAA2B,EAC3B,gBAAgB,EAChB,kCAAkC,GACnC,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,oBAAoB,EACpB,sBAAsB,EACtB,SAAS,EACT,UAAU,GACX,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,uBAAuB,EACvB,yBAAyB,EACzB,UAAU,EACV,UAAU,EACV,aAAa,EACb,qBAAqB,EACrB,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,yBAAyB,EACzB,2BAA2B,EAC3B,2BAA2B,EAC3B,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,EACnB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,yBAAyB,EACzB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,6BAA6B,EAC7B,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,uBAAuB,EACvB,kBAAkB,EAClB,0BAA0B,EAC1B,0BAA0B,EAC1B,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGpE,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,uBAAuB,EACvB,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,GACZ,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { u } from "./useMessageHandler-Bkzrdgfj.js";
|
|
2
2
|
import { C, d, p, u as u2, b } from "./useChatKitLabels-DxW0N5U-.js";
|
|
3
3
|
import { S, c, d as d2, f, u as u3, a, b as b2, e } from "./index-D0DQjdsq.js";
|
|
4
|
-
import { _, a as a2, b as b3, c as c2, C as C2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A, B, D, E } from "./CkHistory.vue_vue_type_script_setup_true_lang-
|
|
4
|
+
import { _, a as a2, b as b3, c as c2, C as C2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A, B, D, F, E } from "./CkHistory.vue_vue_type_script_setup_true_lang-Bv3FuKT4.js";
|
|
5
5
|
import { m as m2 } from "./markdownToPlainText-DN_G-I6g.js";
|
|
6
6
|
export {
|
|
7
7
|
C as CHAT_KIT_LABELS,
|
|
@@ -41,8 +41,9 @@ export {
|
|
|
41
41
|
d as defaultChatKitLabels,
|
|
42
42
|
d2 as defaultDocumentUploadConfig,
|
|
43
43
|
f as defaultEndpoints,
|
|
44
|
-
|
|
44
|
+
F as formatRelativeTime,
|
|
45
45
|
m2 as markdownToPlainText,
|
|
46
|
+
E as preloadCkCodeEditor,
|
|
46
47
|
p as provideChatKitLabels,
|
|
47
48
|
u3 as useChat,
|
|
48
49
|
u2 as useChatKitLabels,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aleph-alpha/chat-kit",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.3",
|
|
4
4
|
"description": "Chat kit package for building chat interfaces with Vue",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -115,8 +115,8 @@
|
|
|
115
115
|
"vue": "^3.5.27",
|
|
116
116
|
"vue-tsc": "^3.2.4",
|
|
117
117
|
"wait-on": "9.0.3",
|
|
118
|
-
"@aleph-alpha/eslint-config-frontend": "0.7.1",
|
|
119
118
|
"@aleph-alpha/prettier-config-frontend": "2.1.1",
|
|
119
|
+
"@aleph-alpha/eslint-config-frontend": "0.7.1",
|
|
120
120
|
"@aleph-alpha/tsconfig-frontend": "0.7.3"
|
|
121
121
|
},
|
|
122
122
|
"scripts": {
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { render, screen, waitFor } from '@testing-library/vue';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import type { MockInstance } from 'vitest';
|
|
3
4
|
import CkCodeEditor from './CkCodeEditor.vue';
|
|
4
5
|
import { detectLineSeparator, resolveLanguage } from './createCodeMirror';
|
|
5
6
|
import { matchBadgeRanges } from './badgeDecoration';
|
|
7
|
+
import { loadCodeMirror } from './loadCodeMirror';
|
|
8
|
+
|
|
9
|
+
// Wrapped so a test can make the lazy chunk fail; every other test gets the
|
|
10
|
+
// real module.
|
|
11
|
+
vi.mock('./loadCodeMirror', async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal<typeof import('./loadCodeMirror')>();
|
|
13
|
+
return { ...actual, loadCodeMirror: vi.fn(actual.loadCodeMirror) };
|
|
14
|
+
});
|
|
6
15
|
|
|
7
16
|
// CodeMirror measures its own layout on construction; jsdom has none of the
|
|
8
17
|
// geometry APIs it calls. The stubs stay in this spec rather than the global
|
|
@@ -273,6 +282,34 @@ describe('CkCodeEditor', () => {
|
|
|
273
282
|
).toBe('0px');
|
|
274
283
|
});
|
|
275
284
|
|
|
285
|
+
it('fades into the design surfaces by default', async () => {
|
|
286
|
+
const { container } = await renderEditor({ modelValue: 'a\nb' });
|
|
287
|
+
|
|
288
|
+
const mask = container.querySelector('.ui-fade-mask') as HTMLElement;
|
|
289
|
+
expect(mask.style.getPropertyValue('--ck-code-fade-top')).toBe(
|
|
290
|
+
'var(--background-surface-secondary)',
|
|
291
|
+
);
|
|
292
|
+
expect(mask.style.getPropertyValue('--ck-code-fade-bottom')).toBe(
|
|
293
|
+
'var(--background-surface-floating)',
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('fades each edge into the surface its prop names', async () => {
|
|
298
|
+
const { container } = await renderEditor({
|
|
299
|
+
modelValue: 'a\nb',
|
|
300
|
+
fadeTopBackground: 'modal',
|
|
301
|
+
fadeBottomBackground: 'primary',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const mask = container.querySelector('.ui-fade-mask') as HTMLElement;
|
|
305
|
+
expect(mask.style.getPropertyValue('--ck-code-fade-top')).toBe(
|
|
306
|
+
'var(--background-surface-modal)',
|
|
307
|
+
);
|
|
308
|
+
expect(mask.style.getPropertyValue('--ck-code-fade-bottom')).toBe(
|
|
309
|
+
'var(--background-surface-primary)',
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
276
313
|
it('omits the fade mask when `scrollFade` is off', async () => {
|
|
277
314
|
const { container } = await renderEditor({
|
|
278
315
|
modelValue: 'a\nb',
|
|
@@ -325,6 +362,68 @@ describe('CkCodeEditor', () => {
|
|
|
325
362
|
const late = await renderEditor({ modelValue: 'y' });
|
|
326
363
|
expect(() => late.unmount()).not.toThrow();
|
|
327
364
|
});
|
|
365
|
+
|
|
366
|
+
describe('when CodeMirror fails to load', () => {
|
|
367
|
+
let logError: MockInstance;
|
|
368
|
+
|
|
369
|
+
beforeEach(() => {
|
|
370
|
+
vi.mocked(loadCodeMirror).mockRejectedValueOnce(
|
|
371
|
+
new Error('Failed to fetch dynamically imported module'),
|
|
372
|
+
);
|
|
373
|
+
logError = vi
|
|
374
|
+
.spyOn(globalThis.console, 'error')
|
|
375
|
+
.mockImplementation(() => undefined);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
afterEach(() => {
|
|
379
|
+
logError.mockRestore();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
async function renderFallback(
|
|
383
|
+
props: InstanceType<typeof CkCodeEditor>['$props'] = {},
|
|
384
|
+
) {
|
|
385
|
+
const utils = render(CkCodeEditor, { props });
|
|
386
|
+
const textarea = await waitFor(() => {
|
|
387
|
+
const found = utils.container.querySelector(
|
|
388
|
+
'.ck-code-editor--fallback textarea',
|
|
389
|
+
);
|
|
390
|
+
expect(found).toBeInTheDocument();
|
|
391
|
+
return found as HTMLTextAreaElement;
|
|
392
|
+
});
|
|
393
|
+
return { ...utils, textarea };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
it('falls back to a textarea with the value, placeholder and label', async () => {
|
|
397
|
+
const { textarea } = await renderFallback({
|
|
398
|
+
modelValue: 'body',
|
|
399
|
+
placeholder: 'Write the skill',
|
|
400
|
+
ariaLabel: 'Skill body',
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
expect(textarea.value).toBe('body');
|
|
404
|
+
expect(textarea.placeholder).toBe('Write the skill');
|
|
405
|
+
expect(textarea.getAttribute('aria-label')).toBe('Skill body');
|
|
406
|
+
expect(logError).toHaveBeenCalled();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('keeps v-model working through the textarea', async () => {
|
|
410
|
+
const { textarea, emitted } = await renderFallback({ modelValue: '' });
|
|
411
|
+
|
|
412
|
+
await userEvent.type(textarea, 'hi');
|
|
413
|
+
|
|
414
|
+
const updates = emitted()['update:modelValue'] as string[][];
|
|
415
|
+
expect(updates.at(-1)?.[0]).toBe('hi');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('locks the textarea when readonly', async () => {
|
|
419
|
+
const { textarea } = await renderFallback({
|
|
420
|
+
modelValue: 'x',
|
|
421
|
+
readonly: true,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
expect(textarea.readOnly).toBe(true);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
328
427
|
});
|
|
329
428
|
|
|
330
429
|
describe('resolveLanguage', () => {
|
|
@@ -58,6 +58,16 @@ const meta: Meta<typeof CkCodeEditor> = {
|
|
|
58
58
|
lineWrapping: { control: 'boolean' },
|
|
59
59
|
autofocus: { control: 'boolean' },
|
|
60
60
|
scrollFade: { control: 'boolean' },
|
|
61
|
+
fadeTopBackground: {
|
|
62
|
+
control: 'inline-radio',
|
|
63
|
+
options: ['primary', 'secondary', 'floating', 'modal'],
|
|
64
|
+
description: 'Surface the top scroll fade dissolves into.',
|
|
65
|
+
},
|
|
66
|
+
fadeBottomBackground: {
|
|
67
|
+
control: 'inline-radio',
|
|
68
|
+
options: ['primary', 'secondary', 'floating', 'modal'],
|
|
69
|
+
description: 'Surface the bottom scroll fade dissolves into.',
|
|
70
|
+
},
|
|
61
71
|
// A regex plus a callback cannot be typed into a control, so the rule is
|
|
62
72
|
// offered as named presets instead. Without this, badges could only be seen
|
|
63
73
|
// on the one story that hard-codes a rule.
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { UiButton, UiFadeMask, UiIcon } from '@aleph-alpha/ui-library';
|
|
12
12
|
import { useChatKitLabels } from '../../../handlers/useChatKitLabels';
|
|
13
13
|
import type { CodeMirrorHandle } from './createCodeMirror';
|
|
14
|
+
import { loadCodeMirror } from './loadCodeMirror';
|
|
14
15
|
import type { CkCodeEditorEmits, CkCodeEditorProps } from './types';
|
|
15
16
|
|
|
16
17
|
defineOptions({ name: 'CkCodeEditor' });
|
|
@@ -30,6 +31,8 @@ const props = withDefaults(defineProps<CkCodeEditorProps>(), {
|
|
|
30
31
|
maxHeight: undefined,
|
|
31
32
|
autofocus: false,
|
|
32
33
|
scrollFade: true,
|
|
34
|
+
fadeTopBackground: 'secondary',
|
|
35
|
+
fadeBottomBackground: 'floating',
|
|
33
36
|
badge: undefined,
|
|
34
37
|
writeToClipboard: undefined,
|
|
35
38
|
});
|
|
@@ -39,8 +42,12 @@ const emit = defineEmits<CkCodeEditorEmits>();
|
|
|
39
42
|
const labels = useChatKitLabels();
|
|
40
43
|
const rootRef = useTemplateRef<HTMLDivElement>('rootRef');
|
|
41
44
|
const hostRef = useTemplateRef<HTMLDivElement>('hostRef');
|
|
45
|
+
const fallbackRef = useTemplateRef<HTMLTextAreaElement>('fallbackRef');
|
|
42
46
|
const isLocked = computed(() => props.readonly || props.disabled);
|
|
43
47
|
const isReady = ref(false);
|
|
48
|
+
// CodeMirror could not be loaded or built; a plain textarea stands in so the
|
|
49
|
+
// field can still be typed into.
|
|
50
|
+
const isFallback = ref(false);
|
|
44
51
|
const isCopied = ref(false);
|
|
45
52
|
|
|
46
53
|
// `UiCodeBlock` labels the block with the filename when there is one and hides
|
|
@@ -84,8 +91,11 @@ const fadeProps = computed(() => {
|
|
|
84
91
|
const down = canScrollDown.value;
|
|
85
92
|
return {
|
|
86
93
|
position: edgeToFade(up, down),
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
style: {
|
|
95
|
+
'--ui-fade-mask-height': up || down ? FADE_HEIGHT : '0px',
|
|
96
|
+
'--ck-code-fade-top': `var(--background-surface-${props.fadeTopBackground})`,
|
|
97
|
+
'--ck-code-fade-bottom': `var(--background-surface-${props.fadeBottomBackground})`,
|
|
98
|
+
},
|
|
89
99
|
};
|
|
90
100
|
});
|
|
91
101
|
|
|
@@ -125,19 +135,16 @@ function handleChange(value: string): void {
|
|
|
125
135
|
emit('update:modelValue', value);
|
|
126
136
|
}
|
|
127
137
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Lazy: CodeMirror and its grammars load only once an editor is shown.
|
|
133
|
-
const { createCodeMirror } = await import('./createCodeMirror');
|
|
138
|
+
async function mountEditor(parent: HTMLElement): Promise<void> {
|
|
139
|
+
// Lazy: CodeMirror loads only once an editor is shown. The grammar follows in
|
|
140
|
+
// the background, so a slow one delays the colours, not the editor.
|
|
141
|
+
const { createCodeMirror } = await loadCodeMirror();
|
|
134
142
|
if (destroyed) return;
|
|
135
143
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const handle = await createCodeMirror({
|
|
144
|
+
lastKnownCode = props.modelValue;
|
|
145
|
+
const handle = createCodeMirror({
|
|
139
146
|
parent,
|
|
140
|
-
doc:
|
|
147
|
+
doc: props.modelValue,
|
|
141
148
|
language: props.language,
|
|
142
149
|
placeholder: props.placeholder,
|
|
143
150
|
ariaLabel: props.ariaLabel,
|
|
@@ -149,20 +156,39 @@ onMounted(async () => {
|
|
|
149
156
|
onFocus: () => emit('focus'),
|
|
150
157
|
});
|
|
151
158
|
|
|
152
|
-
// Unmounted while the grammar was loading.
|
|
153
|
-
if (destroyed) {
|
|
154
|
-
handle.destroy();
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
159
|
editor = handle;
|
|
159
|
-
// Props may have moved on while the grammar loaded; catch up.
|
|
160
|
-
if (props.modelValue !== initialCode) handle.setCode(props.modelValue);
|
|
161
160
|
watchScrolling(handle.view.scrollDOM, handle.view.contentDOM);
|
|
162
161
|
isReady.value = true;
|
|
163
162
|
if (props.autofocus) handle.focus();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
onMounted(async () => {
|
|
166
|
+
const parent = hostRef.value;
|
|
167
|
+
if (!parent) return;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await mountEditor(parent);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
// A stale or unreachable chunk, or a CodeMirror setup error such as
|
|
173
|
+
// duplicate `@codemirror/state` copies. Left alone, the header would sit
|
|
174
|
+
// over an empty body with no way to type.
|
|
175
|
+
// eslint-disable-next-line no-console
|
|
176
|
+
console.error('[CkCodeEditor] Falling back to a plain textarea:', error);
|
|
177
|
+
if (destroyed) return;
|
|
178
|
+
editor?.destroy();
|
|
179
|
+
editor = undefined;
|
|
180
|
+
isFallback.value = true;
|
|
181
|
+
if (props.autofocus) {
|
|
182
|
+
await nextTick();
|
|
183
|
+
fallbackRef.value?.focus();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
164
186
|
});
|
|
165
187
|
|
|
188
|
+
function onFallbackInput(event: Event): void {
|
|
189
|
+
handleChange((event.target as HTMLTextAreaElement).value);
|
|
190
|
+
}
|
|
191
|
+
|
|
166
192
|
watch(
|
|
167
193
|
() => props.modelValue,
|
|
168
194
|
(next) => {
|
|
@@ -234,7 +260,8 @@ function onFocusOut(event: FocusEvent): void {
|
|
|
234
260
|
}
|
|
235
261
|
|
|
236
262
|
function focus(): void {
|
|
237
|
-
editor
|
|
263
|
+
if (editor) editor.focus();
|
|
264
|
+
else fallbackRef.value?.focus();
|
|
238
265
|
}
|
|
239
266
|
|
|
240
267
|
function getCode(): string {
|
|
@@ -250,6 +277,7 @@ defineExpose({ focus, getCode });
|
|
|
250
277
|
:class="[
|
|
251
278
|
'ck-code-editor ck-code-surface border-border-surface-default bg-background-input-default min-w-0 overflow-hidden rounded-xl border',
|
|
252
279
|
isReady && 'ck-code-editor--ready',
|
|
280
|
+
isFallback && 'ck-code-editor--fallback',
|
|
253
281
|
isLocked && 'ck-code-editor--readonly',
|
|
254
282
|
disabled && 'ck-code-editor--disabled',
|
|
255
283
|
]"
|
|
@@ -283,7 +311,22 @@ defineExpose({ focus, getCode });
|
|
|
283
311
|
ref="hostRef"
|
|
284
312
|
class="ck-code-editor__host"
|
|
285
313
|
:style="maxHeight ? { '--ck-code-max-height': maxHeight } : undefined"
|
|
286
|
-
|
|
314
|
+
>
|
|
315
|
+
<textarea
|
|
316
|
+
v-if="isFallback"
|
|
317
|
+
ref="fallbackRef"
|
|
318
|
+
class="ck-code-editor__fallback"
|
|
319
|
+
:value="modelValue"
|
|
320
|
+
:placeholder="placeholder"
|
|
321
|
+
:aria-label="ariaLabel"
|
|
322
|
+
:readonly="readonly"
|
|
323
|
+
:disabled="disabled"
|
|
324
|
+
:wrap="lineWrapping ? 'soft' : 'off'"
|
|
325
|
+
spellcheck="false"
|
|
326
|
+
@input="onFallbackInput"
|
|
327
|
+
@focus="emit('focus')"
|
|
328
|
+
/>
|
|
329
|
+
</div>
|
|
287
330
|
</component>
|
|
288
331
|
</div>
|
|
289
332
|
</template>
|
|
@@ -296,19 +339,15 @@ defineExpose({ focus, getCode });
|
|
|
296
339
|
* `theme.ts`, because `CkMilkdownEditor`'s code blocks use that theme too and
|
|
297
340
|
* must keep matching the rendered block. */
|
|
298
341
|
.ck-code-editor {
|
|
342
|
+
/* `theme.ts` paints the editor from these; transparent lets the root's
|
|
343
|
+
* input surface show through. */
|
|
299
344
|
--ck-cm-bg: transparent;
|
|
300
345
|
--ck-cm-foreground: oklch(var(--content-on-surface-primary));
|
|
301
346
|
--ck-cm-line-number: oklch(var(--content-on-surface-muted));
|
|
302
347
|
}
|
|
303
348
|
|
|
304
|
-
/*
|
|
305
|
-
*
|
|
306
|
-
.ck-code-editor__host :deep(.cm-editor) {
|
|
307
|
-
background: var(--ck-cm-bg);
|
|
308
|
-
color: var(--ck-cm-foreground);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/* Same declarations as the `body-14-regular` typography utility. */
|
|
349
|
+
/* `:deep` is needed below because CodeMirror builds its own DOM, which scoped
|
|
350
|
+
* styles do not reach. Same declarations as the `body-14-regular` typography utility. */
|
|
312
351
|
.ck-code-editor__host :deep(.cm-scroller) {
|
|
313
352
|
max-height: var(--ck-code-max-height, none);
|
|
314
353
|
font-family: Saans, sans-serif;
|
|
@@ -343,14 +382,64 @@ defineExpose({ focus, getCode });
|
|
|
343
382
|
font-weight: 500;
|
|
344
383
|
}
|
|
345
384
|
|
|
385
|
+
/* A column so the stand-in below grows into a host a consumer has stretched
|
|
386
|
+
* (as a fixed-height dialog does), rather than sitting at its minimum height
|
|
387
|
+
* at the top of an empty panel. */
|
|
388
|
+
.ck-code-editor--fallback .ck-code-editor__host {
|
|
389
|
+
display: flex;
|
|
390
|
+
flex-direction: column;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* Stand-in for when CodeMirror fails to load: same text metrics and insets as
|
|
394
|
+
* the editor without a gutter, so the field reads as the same surface. Where
|
|
395
|
+
* supported, it grows with its content the way the editor does. */
|
|
396
|
+
.ck-code-editor__fallback {
|
|
397
|
+
display: block;
|
|
398
|
+
flex: 1 1 auto;
|
|
399
|
+
width: 100%;
|
|
400
|
+
min-height: 5rem;
|
|
401
|
+
field-sizing: content;
|
|
402
|
+
max-height: var(--ck-code-max-height, none);
|
|
403
|
+
resize: none;
|
|
404
|
+
border: 0;
|
|
405
|
+
outline: none;
|
|
406
|
+
padding: 0.75rem 1.25rem 1rem 1rem;
|
|
407
|
+
background: var(--ck-cm-bg);
|
|
408
|
+
color: var(--ck-cm-foreground);
|
|
409
|
+
font-family: Saans, sans-serif;
|
|
410
|
+
font-size: 0.875rem;
|
|
411
|
+
font-weight: 400;
|
|
412
|
+
line-height: 1.25rem;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.ck-code-editor__fallback::placeholder,
|
|
346
416
|
.ck-code-editor__host :deep(.cm-placeholder) {
|
|
347
417
|
color: oklch(var(--content-on-surface-muted));
|
|
348
418
|
}
|
|
349
419
|
|
|
350
|
-
/*
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
420
|
+
/* Each band fades into its own surface (`fadeTopBackground` /
|
|
421
|
+
* `fadeBottomBackground`), whereas `UiFadeMask`'s `background` sets one for
|
|
422
|
+
* both and only knows `primary` and `secondary`. The gradient, edges and band
|
|
423
|
+
* height stay the mask's own. `[data-background]` outranks the mask's
|
|
424
|
+
* per-surface rule regardless of stylesheet order. */
|
|
425
|
+
.ck-code-editor__body
|
|
426
|
+
:deep(.ui-fade-mask__band[data-background][data-position='top']) {
|
|
427
|
+
--ui-fade-mask-surface: var(--ck-code-fade-top);
|
|
428
|
+
/* The mask turns its top band opaque halfway down on desktop, which left a
|
|
429
|
+
* solid strip under the header; mirror the bottom band's stops instead. */
|
|
430
|
+
--ui-fade-mask-stop: 20%;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.ck-code-editor__body
|
|
434
|
+
:deep(.ui-fade-mask__band[data-background][data-position='bottom']) {
|
|
435
|
+
--ui-fade-mask-surface: var(--ck-code-fade-bottom);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
@media (min-width: 768px) {
|
|
439
|
+
.ck-code-editor__body
|
|
440
|
+
:deep(.ui-fade-mask__band[data-background][data-position='top']) {
|
|
441
|
+
--ui-fade-mask-stop: 100%;
|
|
442
|
+
}
|
|
354
443
|
}
|
|
355
444
|
|
|
356
445
|
.ck-code-editor--disabled {
|
|
@@ -151,10 +151,14 @@ function baseExtensions(options: CreateCodeMirrorOptions): Extension[] {
|
|
|
151
151
|
/**
|
|
152
152
|
* Build a CodeMirror view themed like `UiCodeBlock`. Loaded lazily by
|
|
153
153
|
* `CkCodeEditor` so CodeMirror stays out of the package's main chunk.
|
|
154
|
+
*
|
|
155
|
+
* The view is built at once, as plain text, and the grammar is applied when it
|
|
156
|
+
* arrives: the placeholder, gutter and content must not wait on a network
|
|
157
|
+
* request that may be slow or never finish. Only the syntax colours do.
|
|
154
158
|
*/
|
|
155
|
-
export
|
|
159
|
+
export function createCodeMirror(
|
|
156
160
|
options: CreateCodeMirrorOptions,
|
|
157
|
-
):
|
|
161
|
+
): CodeMirrorHandle {
|
|
158
162
|
// One set per view: a compartment is the key a reconfigure targets, so
|
|
159
163
|
// sharing them across editors would make the instances interfere.
|
|
160
164
|
const languageSlot = new Compartment();
|
|
@@ -164,9 +168,10 @@ export async function createCodeMirror(
|
|
|
164
168
|
const badgeSlot = new Compartment();
|
|
165
169
|
const lineSeparatorSlot = new Compartment();
|
|
166
170
|
|
|
167
|
-
const support = await resolveLanguage(options.language);
|
|
168
171
|
// Guards against out-of-order grammar loads; see `setLanguage` below.
|
|
169
172
|
let languageRequestId = 0;
|
|
173
|
+
// A grammar can land after the view is gone; it must not touch it then.
|
|
174
|
+
let destroyed = false;
|
|
170
175
|
|
|
171
176
|
const view = new EditorView({
|
|
172
177
|
parent: options.parent,
|
|
@@ -174,7 +179,7 @@ export async function createCodeMirror(
|
|
|
174
179
|
doc: options.doc,
|
|
175
180
|
extensions: [
|
|
176
181
|
...baseExtensions(options),
|
|
177
|
-
languageSlot.of(
|
|
182
|
+
languageSlot.of([]),
|
|
178
183
|
readonlySlot.of(readonlyState(options.readonly)),
|
|
179
184
|
lineNumbersSlot.of(options.showLineNumbers ? lineNumbers() : []),
|
|
180
185
|
wrappingSlot.of(options.lineWrapping ? EditorView.lineWrapping : []),
|
|
@@ -187,9 +192,29 @@ export async function createCodeMirror(
|
|
|
187
192
|
});
|
|
188
193
|
|
|
189
194
|
function reconfigure(slot: Compartment, extension: Extension): void {
|
|
195
|
+
if (destroyed) return;
|
|
190
196
|
view.dispatch({ effects: slot.reconfigure(extension) });
|
|
191
197
|
}
|
|
192
198
|
|
|
199
|
+
async function setLanguage(language: string): Promise<void> {
|
|
200
|
+
// Grammars load over the network and out of order, so a slow one must not
|
|
201
|
+
// land on top of a language chosen after it.
|
|
202
|
+
const request = ++languageRequestId;
|
|
203
|
+
const next = await resolveLanguage(language);
|
|
204
|
+
if (request !== languageRequestId) return;
|
|
205
|
+
try {
|
|
206
|
+
reconfigure(languageSlot, next ? [next] : []);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// Typically a grammar built against another `@codemirror/state` copy.
|
|
209
|
+
// The editor keeps working as plain text; say why the colours are gone.
|
|
210
|
+
// eslint-disable-next-line no-console
|
|
211
|
+
console.warn(`[CkCodeEditor] Could not apply "${language}":`, error);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Nothing waits on this; a failed grammar already resolves to plain text.
|
|
216
|
+
setLanguage(options.language).catch(() => undefined);
|
|
217
|
+
|
|
193
218
|
return {
|
|
194
219
|
view,
|
|
195
220
|
getCode: () => readDoc(view.state),
|
|
@@ -208,14 +233,7 @@ export async function createCodeMirror(
|
|
|
208
233
|
selection: { anchor: cursor },
|
|
209
234
|
});
|
|
210
235
|
},
|
|
211
|
-
setLanguage
|
|
212
|
-
// Grammars load over the network and out of order, so a slow one must not
|
|
213
|
-
// land on top of a language chosen after it.
|
|
214
|
-
const request = ++languageRequestId;
|
|
215
|
-
const next = await resolveLanguage(language);
|
|
216
|
-
if (request !== languageRequestId) return;
|
|
217
|
-
reconfigure(languageSlot, next ? [next] : []);
|
|
218
|
-
},
|
|
236
|
+
setLanguage,
|
|
219
237
|
setReadonly: (readonly) => {
|
|
220
238
|
reconfigure(readonlySlot, readonlyState(readonly));
|
|
221
239
|
},
|
|
@@ -229,6 +247,9 @@ export async function createCodeMirror(
|
|
|
229
247
|
reconfigure(badgeSlot, rule ? badgeExtension(rule) : []);
|
|
230
248
|
},
|
|
231
249
|
focus: () => view.focus(),
|
|
232
|
-
destroy: () =>
|
|
250
|
+
destroy: () => {
|
|
251
|
+
destroyed = true;
|
|
252
|
+
view.destroy();
|
|
253
|
+
},
|
|
233
254
|
};
|
|
234
255
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type * as CodeMirrorModule from './createCodeMirror';
|
|
2
|
+
|
|
3
|
+
let pending: Promise<typeof CodeMirrorModule> | undefined;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Import the CodeMirror chunk once and share the result.
|
|
7
|
+
*
|
|
8
|
+
* A failed import is forgotten rather than cached, so the next editor to mount
|
|
9
|
+
* (or the next `preloadCkCodeEditor` call) tries again instead of inheriting the
|
|
10
|
+
* failure.
|
|
11
|
+
*/
|
|
12
|
+
export function loadCodeMirror(): Promise<typeof CodeMirrorModule> {
|
|
13
|
+
pending ??= import('./createCodeMirror').catch((error: unknown) => {
|
|
14
|
+
pending = undefined;
|
|
15
|
+
throw error;
|
|
16
|
+
});
|
|
17
|
+
return pending;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Start fetching `CkCodeEditor`'s CodeMirror chunk ahead of time, for example
|
|
22
|
+
* when a view that will open an editor is shown. The editor then appears
|
|
23
|
+
* without waiting on the network. Safe to call repeatedly; failures are
|
|
24
|
+
* swallowed here and surface, if still failing, when an editor mounts.
|
|
25
|
+
*/
|
|
26
|
+
export function preloadCkCodeEditor(): void {
|
|
27
|
+
loadCodeMirror().catch(() => undefined);
|
|
28
|
+
}
|