@basthon/gui-base 0.37.2 → 0.38.0

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/lib/main.d.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import { KernelBase } from "@basthon/kernel-base";
2
2
  import { KernelLoader } from "@basthon/kernel-loader";
3
+ declare global {
4
+ interface Window {
5
+ _basthonEmptyContent?: any;
6
+ }
7
+ }
8
+ export interface GUIOptions {
9
+ kernelRootPath: string;
10
+ language: string;
11
+ uiName?: string;
12
+ }
3
13
  /**
4
14
  * Base class for console and notebook GUI.
5
15
  */
@@ -7,8 +17,12 @@ export declare class GUIBase {
7
17
  private readonly _language;
8
18
  private readonly _loaded;
9
19
  private _loader;
20
+ private _checkpoints;
21
+ private _maxCheckpoints;
22
+ protected _contentFilename: string;
23
+ protected _urlKey: string;
10
24
  private _console_error;
11
- constructor(kernelRootPath: string, language: string);
25
+ constructor(options: GUIOptions);
12
26
  /**
13
27
  * Language getter.
14
28
  */
@@ -53,6 +67,46 @@ export declare class GUIBase {
53
67
  * Initialize the GUI.
54
68
  */
55
69
  init(options?: any): Promise<void>;
70
+ /**
71
+ * Get the content (script or notebook content).
72
+ */
73
+ protected content(): string;
74
+ /**
75
+ * Set the content (script or notebook content).
76
+ */
77
+ protected setContent(content: string): void;
78
+ /**
79
+ * Loading the content from query string (ipynb=/script= or from=).
80
+ */
81
+ loadFromQS(key: string): Promise<string | null>;
82
+ /**
83
+ * Setup the UI (typically connect events etc..).
84
+ */
85
+ protected setupUI(options: any): Promise<void>;
86
+ /**
87
+ * Load content at startup.
88
+ */
89
+ protected loadInitialContent(options: any): Promise<void>;
90
+ /**
91
+ * Load content from local forage.
92
+ */
93
+ protected loadFromStorage(): Promise<string | null>;
94
+ /**
95
+ * Tag last backup as "approved".
96
+ */
97
+ validateBackup(): Promise<void>;
98
+ /**
99
+ * Select a checkpoint to load in the script.
100
+ */
101
+ selectCheckpoint(): Promise<string | null>;
102
+ /**
103
+ * Backup to checkpoints.
104
+ */
105
+ backup(approved?: boolean): Promise<string>;
106
+ /**
107
+ * Internal GUI init.
108
+ * It calls setupUI then loadInitialContent.
109
+ */
56
110
  protected _init(options?: any): Promise<void>;
57
111
  /**
58
112
  * Change loader text and call init function.
@@ -97,10 +151,22 @@ export declare class GUIBase {
97
151
  * Open an URL in a new tab or download a file.
98
152
  */
99
153
  static openURL(url: string, download?: string): void;
154
+ /**
155
+ * Returning the sharing link for the content.
156
+ */
157
+ sharingURL(key: string): Promise<string>;
100
158
  /**
101
159
  * Share content via URL.
102
160
  */
103
- protected _share(url: string, message: string): Promise<void>;
161
+ protected share(): Promise<void>;
162
+ /**
163
+ * Opening file (async) and load its content.
164
+ */
165
+ open(file: Blob | File): Promise<void>;
166
+ /**
167
+ * Download content to file.
168
+ */
169
+ download(): void;
104
170
  /**
105
171
  * Copy a text to clipboard.
106
172
  */
package/lib/main.js CHANGED
@@ -1,18 +1,24 @@
1
1
  import { PromiseDelegate } from "promise-delegate";
2
+ import { CheckpointsManager } from "@basthon/checkpoints";
2
3
  import { KernelBase } from "@basthon/kernel-base";
3
4
  import { KernelLoader } from "@basthon/kernel-loader";
4
5
  /**
5
6
  * Base class for console and notebook GUI.
6
7
  */
7
8
  export class GUIBase {
8
- constructor(kernelRootPath, language) {
9
+ constructor(options) {
9
10
  this._loaded = new PromiseDelegate();
11
+ this._maxCheckpoints = 5;
12
+ this._contentFilename = "content.txt";
13
+ this._urlKey = "content";
10
14
  /* console errors redirected to notification system */
11
15
  this._console_error = console.error;
12
- this._language = language;
13
- this._loader = new KernelLoader(kernelRootPath, language);
16
+ this._language = options.language;
17
+ this._loader = new KernelLoader(options.kernelRootPath, options.language);
14
18
  // loading Basthon (errors are fatal)
15
19
  this._loader.showLoader("Chargement de Basthon...", false, false);
20
+ /* per language checkpoints */
21
+ this._checkpoints = new CheckpointsManager(`${options.uiName}.${options.language}`, this._maxCheckpoints);
16
22
  }
17
23
  /**
18
24
  * Language getter.
@@ -78,12 +84,154 @@ export class GUIBase {
78
84
  this._loaded.reject(error);
79
85
  }
80
86
  }
87
+ /**
88
+ * Get the content (script or notebook content).
89
+ */
90
+ content() { return ""; }
91
+ /**
92
+ * Set the content (script or notebook content).
93
+ */
94
+ setContent(content) { }
95
+ /**
96
+ * Loading the content from query string (ipynb=/script= or from=).
97
+ */
98
+ async loadFromQS(key) {
99
+ const url = new URL(window.location.href);
100
+ const from_key = 'from';
101
+ let content = null;
102
+ if (url.searchParams.has(key)) {
103
+ content = url.searchParams.get(key) || "";
104
+ try {
105
+ content = await this.inflate(content);
106
+ }
107
+ catch (error) {
108
+ /* backward compatibility with non compressed param */
109
+ if (content != null)
110
+ content = decodeURIComponent(content);
111
+ }
112
+ }
113
+ else if (url.searchParams.has(from_key)) {
114
+ let fileURL = url.searchParams.get(from_key);
115
+ if (fileURL != null)
116
+ fileURL = decodeURIComponent(fileURL);
117
+ try {
118
+ content = await GUIBase.xhr({
119
+ url: fileURL,
120
+ method: 'GET'
121
+ });
122
+ }
123
+ catch (error) {
124
+ const message = `Le chargement de ${fileURL} a échoué.`;
125
+ throw new ErrorEvent(message, { message: message });
126
+ }
127
+ }
128
+ if (content != null)
129
+ this.setContent(content);
130
+ return content;
131
+ }
132
+ /**
133
+ * Setup the UI (typically connect events etc..).
134
+ */
135
+ async setupUI(options) {
136
+ this.kernelLoader.kernelAvailable().then((kernel) => {
137
+ // this is ugly but it seems complicated to make
138
+ // GUI available in websocket for the notebook
139
+ // so patch it!
140
+ const dispatchEvent = kernel.dispatchEvent.bind(this.kernel);
141
+ kernel.dispatchEvent = async (event, data) => {
142
+ if (event === "eval.request")
143
+ await this.backup(false);
144
+ dispatchEvent(event, data);
145
+ };
146
+ kernel.addEventListener("eval.finished", () => {
147
+ this.validateBackup();
148
+ });
149
+ kernel.addEventListener("eval.error", () => {
150
+ this.validateBackup();
151
+ });
152
+ });
153
+ }
154
+ /**
155
+ * Load content at startup.
156
+ */
157
+ async loadInitialContent(options) {
158
+ if (window._basthonEmptyContent)
159
+ return;
160
+ if (await this.loadFromQS(this._urlKey) != null)
161
+ return;
162
+ this.loadFromStorage();
163
+ }
164
+ /**
165
+ * Load content from local forage.
166
+ */
167
+ async loadFromStorage() {
168
+ const approved = await this._checkpoints.lastHasTag("approved");
169
+ let content = null;
170
+ if (approved == null) {
171
+ content = null;
172
+ }
173
+ else if (approved) {
174
+ content = await this._checkpoints.getLast();
175
+ }
176
+ else {
177
+ const promise = new PromiseDelegate();
178
+ this.confirm("Récupération", "Il semble que Basthon ait rencontré un problème à sa dernière utilisation. Que voulez-vous faire ?", "Choisir une sauvegarde", async () => {
179
+ promise.resolve(await this.selectCheckpoint());
180
+ }, "Laisser le script vide", () => { promise.resolve(""); });
181
+ content = await promise.promise;
182
+ }
183
+ if (content != null)
184
+ this.setContent(content);
185
+ return content;
186
+ }
187
+ /**
188
+ * Tag last backup as "approved".
189
+ */
190
+ async validateBackup() {
191
+ await this._checkpoints.tagLast("approved");
192
+ }
193
+ /**
194
+ * Select a checkpoint to load in the script.
195
+ */
196
+ async selectCheckpoint() {
197
+ const times = await this._checkpoints.times();
198
+ const promise = new PromiseDelegate();
199
+ const choices = times === null || times === void 0 ? void 0 : times.map((t, i) => ({
200
+ text: CheckpointsManager.toHumanDate(parseInt(t, 10)),
201
+ handler: async () => { var _a; promise.resolve((_a = (await this._checkpoints.getIndex(i))) !== null && _a !== void 0 ? _a : null); }
202
+ }));
203
+ if (choices == null)
204
+ return null;
205
+ this.select("Choisissez une sauvegarde", "De quelle sauvegarde souhaitez-vous reprendre ?", choices, "Annuler", () => { promise.resolve(null); });
206
+ return await promise.promise;
207
+ }
208
+ /**
209
+ * Backup to checkpoints.
210
+ */
211
+ async backup(approved = true) {
212
+ return await this._checkpoints.push(this.content(), approved ? ["approved"] : []);
213
+ }
214
+ /**
215
+ * Internal GUI init.
216
+ * It calls setupUI then loadInitialContent.
217
+ */
81
218
  async _init(options = null) {
82
219
  /* all errors redirected to notification system */
83
220
  const onerror = this.notifyError.bind(this);
84
221
  window.addEventListener('error', onerror);
85
222
  window.addEventListener("unhandledrejection", onerror);
86
223
  console.error = (message) => onerror(new ErrorEvent(message, { message: message }));
224
+ await this._checkpoints.ready();
225
+ await this.setupUI(options);
226
+ await this.loadInitialContent(options);
227
+ await this.kernelLoader.kernelLoaded();
228
+ const init = this.initCaller.bind(this);
229
+ // loading aux files from URL
230
+ await init(this.loadURLAux.bind(this), "Chargement des fichiers auxiliaires...", true);
231
+ // loading modules from URL
232
+ await init(this.loadURLModules.bind(this), "Chargement des modules annexes...", true);
233
+ // end
234
+ this.kernelLoader.hideLoader();
87
235
  }
88
236
  /**
89
237
  * Change loader text and call init function.
@@ -211,11 +359,58 @@ export class GUIBase {
211
359
  anchor.click();
212
360
  document.body.removeChild(anchor);
213
361
  }
362
+ /**
363
+ * Returning the sharing link for the content.
364
+ */
365
+ async sharingURL(key) {
366
+ // to retain the Line breaks.
367
+ let content = this.content().replace(/\r\n|\r|\n/g, "\r\n");
368
+ const url = new URL(window.location.href);
369
+ url.hash = "";
370
+ url.searchParams.delete("from"); // take care of collapsing params
371
+ try {
372
+ content = await this.deflate(content);
373
+ }
374
+ catch (e) { // fallback
375
+ content = encodeURIComponent(content).replace(/\(/g, '%28').replace(/\)/g, '%29');
376
+ }
377
+ url.searchParams.set(key, content);
378
+ return url.href;
379
+ }
214
380
  /**
215
381
  * Share content via URL.
216
382
  */
217
- async _share(url, message) {
218
- this.confirm("Partager ce notebook", message, "Copier dans le presse-papier", () => GUIBase.copyToClipboard(url), "Tester le lien", () => GUIBase.openURL(url));
383
+ async share() {
384
+ const url = await this.sharingURL(this._urlKey);
385
+ const message = `
386
+ Un lien permanant vers le contenu actuel a été créé.
387
+ <br>
388
+ <i class="fas fa-exclamation-circle"></i> Attention, partager un script trop long peut ne pas fonctionner avec certains navigateurs.`;
389
+ this.confirm("Partager ce document", message, "Copier dans le presse-papier", () => GUIBase.copyToClipboard(url), "Tester le lien", () => GUIBase.openURL(url));
390
+ }
391
+ /**
392
+ * Opening file (async) and load its content.
393
+ */
394
+ open(file) {
395
+ return new Promise((resolve, reject) => {
396
+ const reader = new FileReader();
397
+ reader.readAsText(file);
398
+ reader.onload = (event) => {
399
+ var _a;
400
+ this.setContent((_a = event === null || event === void 0 ? void 0 : event.target) === null || _a === void 0 ? void 0 : _a.result);
401
+ resolve();
402
+ };
403
+ reader.onerror = reject;
404
+ });
405
+ }
406
+ /**
407
+ * Download content to file.
408
+ */
409
+ download() {
410
+ this.backup();
411
+ const content = this.content().replace(/\r\n|\r|\n/g, "\r\n"); // To retain the Line breaks.
412
+ let blob = new Blob([content], { type: "text/plain" });
413
+ GUIBase.openURL(window.URL.createObjectURL(blob), this._contentFilename);
219
414
  }
220
415
  /**
221
416
  * Binding to kernel's XHR.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basthon/gui-base",
3
- "version": "0.37.2",
3
+ "version": "0.38.0",
4
4
  "description": "Basthon - Base GUI",
5
5
  "homepage": "https://basthon.fr",
6
6
  "bugs": {
@@ -25,8 +25,9 @@
25
25
  "clean": "rm -rf lib/"
26
26
  },
27
27
  "dependencies": {
28
- "@basthon/kernel-base": "^0.37.2",
29
- "@basthon/kernel-loader": "^0.37.2",
28
+ "@basthon/checkpoints": "^0.38.0",
29
+ "@basthon/kernel-base": "^0.38.0",
30
+ "@basthon/kernel-loader": "^0.38.0",
30
31
  "js-base64": "^3.7.2",
31
32
  "pako": "^2.0.4",
32
33
  "promise-delegate": "^1.0.1"
@@ -40,5 +41,5 @@
40
41
  "publishConfig": {
41
42
  "access": "public"
42
43
  },
43
- "gitHead": "21ef9a596b7e9c098a2b72ddd406cbb50577adb0"
44
+ "gitHead": "c66532cfb78ee398bd303080bce81748b0ee8fa4"
44
45
  }