-
Notifications
You must be signed in to change notification settings - Fork 24
/
Copy pathCodeSnippetPreview.ts
277 lines (254 loc) · 7.39 KB
/
CodeSnippetPreview.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// Copyright (c) 2020, jupytercalpoly
// Distributed under the terms of the BSD-3 Clause License.
import { WidgetTracker, ReactWidget } from '@jupyterlab/apputils';
import { CodeEditor, IEditorServices } from '@jupyterlab/codeeditor';
import { Widget, PanelLayout, Panel } from '@lumino/widgets';
import { Message, MessageLoop } from '@lumino/messaging';
import { PromiseDelegate } from '@lumino/coreutils';
import { ArrayExt } from '@lumino/algorithm';
import { ICodeSnippet, CodeSnippetService } from './CodeSnippetService';
/**
* The class name for preview box.
*/
const PREVIEW_CLASS = 'jp-codeSnippet-preview';
const PREVIEW_CONTENT = 'jp-codeSnippet-preview-content';
const PREVIEW_BODY = 'jp-codeSnippet-preview-body';
/**
* Create and show a preview.
*
* @param options - The preview setup options.
*
*/
export function showPreview(
options: Partial<Preview.IOptions> = {},
editorServices: IEditorServices
): Promise<void> {
//Insert check method to see if the preview is already open
const preview = new Preview(options, editorServices);
if (preview.ready === false) {
return;
}
return preview.launch();
}
/**
* A widget used to show preview
*/
export class Preview extends Widget {
ready: boolean;
_title: string;
_id: number;
editor: CodeEditor.IEditor;
codeSnippet: ICodeSnippet;
editorServices: IEditorServices;
codeSnippetService: CodeSnippetService;
private _hasRefreshedSinceAttach: boolean;
constructor(
options: Partial<Preview.IOptions> = {},
editorServices: IEditorServices
) {
super();
this.ready = true;
this._title = options.title;
this._id = options.id;
this.codeSnippet = options.codeSnippet;
this.editorServices = editorServices;
this.codeSnippetService = CodeSnippetService.getCodeSnippetService();
this.addClass(PREVIEW_CLASS);
const layout = (this.layout = new PanelLayout());
const content = new Panel();
content.addClass(PREVIEW_CONTENT);
content.id = PREVIEW_CONTENT + this._id;
layout.addWidget(content);
if (Preview.tracker.size > 0) {
const previous = Preview.tracker.currentWidget;
previous.reject();
Preview.tracker.dispose();
}
if (this.ready === true) {
void Preview.tracker.add(this);
}
}
/**
* Launch the preview as a modal window.
*/
launch(): Promise<void> {
// Return the existing preview if already open.
if (this._promise) {
return this._promise.promise;
}
const promise = (this._promise = new PromiseDelegate<void>());
const promises = Promise.all(Private.launchQueue);
Private.launchQueue.push(this._promise.promise);
return promises.then(() => {
Widget.attach(this, document.getElementById('jp-main-dock-panel'));
return promise.promise;
});
}
/**
* Reject the current preview with a default reject value.
*
* #### Notes
* Will be a no-op if the preview is not shown.
*/
reject(): void {
if (!this._promise) {
return;
}
this._resolve();
}
/**
* Resolve a button item.
*/
private _resolve(): void {
// Prevent loopback.
const promise = this._promise;
if (!promise) {
this.dispose();
return;
}
this._promise = null;
ArrayExt.removeFirstOf(Private.launchQueue, promise.promise);
this.dispose();
promise.resolve();
}
/**
* Dispose of the resources used by the preview.
*/
dispose(): void {
const promise = this._promise;
if (promise) {
this._promise = null;
promise.reject(void 0);
ArrayExt.removeFirstOf(Private.launchQueue, promise.promise);
}
super.dispose();
}
/**
* A message handler invoked on an `'after-attach'` message.
*/
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this._hasRefreshedSinceAttach = false;
if (this.isVisible) {
this.update();
}
}
onAfterShow(msg: Message): void {
if (!this._hasRefreshedSinceAttach) {
this.update();
}
}
onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
if (!this.editor && document.getElementById(PREVIEW_CONTENT + this._id)) {
const editorFactory = this.editorServices.factoryService.newInlineEditor;
const getMimeTypeByLanguage =
this.editorServices.mimeTypeService.getMimeTypeByLanguage;
let previewFontSize = this.codeSnippetService.settings.get(
'snippetPreviewFontSize'
).composite as number;
if (
this.codeSnippetService.settings.get('snippetPreviewFontSize').user !==
undefined
) {
previewFontSize = this.codeSnippetService.settings.get(
'snippetPreviewFontSize'
).user as number;
}
this.editor = editorFactory({
host: document.getElementById(PREVIEW_CONTENT + this._id),
config: { readOnly: true, fontSize: previewFontSize },
model: new CodeEditor.Model({
value: this.codeSnippet.code,
mimeType: getMimeTypeByLanguage({
name: this.codeSnippet.language,
codemirror_mode: this.codeSnippet.language,
}),
}),
});
}
if (this.isVisible) {
this._hasRefreshedSinceAttach = true;
this.editor.refresh();
}
}
private _promise: PromiseDelegate<void> | null;
}
export namespace Preview {
/**
* The body input types.
*/
export type Body = Widget;
export interface IOptions {
title: string;
id: number;
/**
* The main body element for the preview or a message to display.
* Defaults to an empty string.
*
* #### Notes
* If a widget is given as the body, it will be disposed after the
* preview is resolved. If the widget has a `getValue()` method,
* the method will be called prior to disposal and the value
* will be provided as part of the preview result.
* A string argument will be used as raw `textContent`.
* All `input` and `select` nodes will be wrapped and styled.
*/
body: Body;
codeSnippet: ICodeSnippet;
}
export interface IRenderer {
/**
* Create the body of the preview.
*
* @param value - The input value for the body.
*
* @returns A widget for the body.
*/
createBody(body: Body): Widget;
}
export class Renderer {
/**
* Create the body of the preview.
*
* @param value - The input value for the body.
*
* @returns A widget for the body.
*/
createBody(value: Body): Widget {
let body: Widget;
if (typeof value === 'string') {
body = new Widget({ node: document.createElement('span') });
body.node.textContent = value;
} else if (value instanceof Widget) {
body = value;
} else {
body = ReactWidget.create(value);
// Immediately update the body even though it has not yet attached in
// order to trigger a render of the DOM nodes from the React element.
MessageLoop.sendMessage(body, Widget.Msg.UpdateRequest);
}
body.addClass(PREVIEW_BODY);
return body;
}
}
/**
* The default renderer instance.
*/
export const defaultRenderer = new Renderer();
/**
* The preview widget tracker.
*/
export const tracker = new WidgetTracker<Preview>({
namespace: '@jupyterlab/code_snippet:preview',
});
}
/**
* The namespace for module private data.
*/
namespace Private {
/**
* The queue for launching previews.
*/
export const launchQueue: Promise<void>[] = [];
}