flexdesk 0.16.1 → 0.17.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/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +2217 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3146 -164
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +709 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
- package/dist/chunk-MCIXFNVF.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
- package/dist/chunk-Q36EPT2R.js.map +7 -0
- package/dist/chunk-QWQJUDPD.js +610 -0
- package/dist/chunk-QWQJUDPD.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3146 -164
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3252 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/package.json +3 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +211 -0
- package/src/ui/components/cell_grid.js +648 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +601 -62
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +343 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-B5Q53LAJ.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- package/src/ui/controllers/panel_state_machine.js +0 -634
package/src/help/help_modal.js
CHANGED
|
@@ -2,14 +2,25 @@
|
|
|
2
2
|
* HelpModal - Modal dialog for displaying help content
|
|
3
3
|
*
|
|
4
4
|
* Features:
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
5
|
+
* - Topics as trusted HTML (`topic.html`) or as markdown (`topic.markdown`),
|
|
6
|
+
* rendered safe by `renderMarkdown` with the provider's `markdownOptions`
|
|
7
|
+
* (a highlighter, math, extension rules)
|
|
8
|
+
* - A table of contents for a topic with two or more headings, markdown or
|
|
9
|
+
* html alike: its headings, listed and numbered, the one in view marked,
|
|
10
|
+
* a click that scrolls to one (`createDocumentOutline`)
|
|
7
11
|
* - Topic search
|
|
8
12
|
* - Category browsing
|
|
9
13
|
* - Keyboard navigation (Escape to close)
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
import { helpProvider, helpCategories, helpCopy } from './help_registry.js';
|
|
17
|
+
import { renderMarkdown } from '../ui/utils/markdown.js';
|
|
18
|
+
import { createDocumentOutline } from '../ui/components/document_outline.js';
|
|
19
|
+
|
|
20
|
+
/** The headings a topic's table of contents lists. */
|
|
21
|
+
const TOC_HEADINGS = 'h1, h2, h3, h4, h5, h6';
|
|
22
|
+
/** Fewer headings than this, and a topic shows no table of contents. */
|
|
23
|
+
const TOC_MIN_HEADINGS = 2;
|
|
13
24
|
|
|
14
25
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
15
26
|
// HelpModal Class
|
|
@@ -33,14 +44,22 @@ export class HelpModal {
|
|
|
33
44
|
this._modal = null;
|
|
34
45
|
this._currentTopic = null;
|
|
35
46
|
this._searchQuery = '';
|
|
47
|
+
this._fallback = null;
|
|
48
|
+
this._toc = null;
|
|
36
49
|
this._boundKeyHandler = this._handleKeyDown.bind(this);
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
/**
|
|
40
53
|
* Open the help modal.
|
|
41
54
|
* @param {string} [topicId] - Initial topic to display
|
|
55
|
+
* @param {Object} [options]
|
|
56
|
+
* @param {{title: string, html: string}} [options.fallback] - Shown for
|
|
57
|
+
* `topicId` when the provider has no such topic (its `loadTopic` answers
|
|
58
|
+
* null). A library-owned topic, such as the palette's keyboard shortcuts,
|
|
59
|
+
* uses it so that an application's own topic of that id still wins.
|
|
42
60
|
*/
|
|
43
|
-
async open(topicId = null) {
|
|
61
|
+
async open(topicId = null, { fallback = null } = {}) {
|
|
62
|
+
this._fallback = topicId && fallback ? { id: topicId, topic: fallback } : null;
|
|
44
63
|
// Close any existing modal
|
|
45
64
|
if (_activeModal && _activeModal !== this) {
|
|
46
65
|
_activeModal.close();
|
|
@@ -71,6 +90,7 @@ export class HelpModal {
|
|
|
71
90
|
* Close the help modal.
|
|
72
91
|
*/
|
|
73
92
|
close() {
|
|
93
|
+
this._disposeToc();
|
|
74
94
|
if (this._overlay) {
|
|
75
95
|
document.removeEventListener('keydown', this._boundKeyHandler);
|
|
76
96
|
this._overlay.remove();
|
|
@@ -165,15 +185,15 @@ export class HelpModal {
|
|
|
165
185
|
const icon = this._categories[category]?.icon || 'folder';
|
|
166
186
|
|
|
167
187
|
return `
|
|
168
|
-
<div class="help-category" data-category="${category}">
|
|
188
|
+
<div class="help-category" data-category="${escapeHtml(category)}">
|
|
169
189
|
<div class="help-category__header">
|
|
170
|
-
<span class="material-symbols-outlined">${icon}</span>
|
|
171
|
-
<span>${label}</span>
|
|
190
|
+
<span class="material-symbols-outlined">${escapeHtml(icon)}</span>
|
|
191
|
+
<span>${escapeHtml(label)}</span>
|
|
172
192
|
</div>
|
|
173
193
|
<ul class="help-category__topics">
|
|
174
194
|
${topics.map(topic => `
|
|
175
|
-
<li class="help-topic-item" data-topic="${topic.id}">
|
|
176
|
-
${topic.title}
|
|
195
|
+
<li class="help-topic-item" data-topic="${escapeHtml(topic.id)}">
|
|
196
|
+
${escapeHtml(topic.title)}
|
|
177
197
|
</li>
|
|
178
198
|
`).join('')}
|
|
179
199
|
</ul>
|
|
@@ -200,6 +220,7 @@ export class HelpModal {
|
|
|
200
220
|
* @private
|
|
201
221
|
*/
|
|
202
222
|
_showIndex() {
|
|
223
|
+
this._disposeToc();
|
|
203
224
|
const content = this._modal.querySelector('.help-modal__content');
|
|
204
225
|
const categories = this._service.getCategories();
|
|
205
226
|
|
|
@@ -222,15 +243,15 @@ export class HelpModal {
|
|
|
222
243
|
const icon = this._categories[category]?.icon || 'folder';
|
|
223
244
|
|
|
224
245
|
return `
|
|
225
|
-
<div class="help-index__category" data-category="${category}">
|
|
246
|
+
<div class="help-index__category" data-category="${escapeHtml(category)}">
|
|
226
247
|
<div class="help-index__category-header">
|
|
227
|
-
<span class="material-symbols-outlined">${icon}</span>
|
|
228
|
-
<span>${label}</span>
|
|
248
|
+
<span class="material-symbols-outlined">${escapeHtml(icon)}</span>
|
|
249
|
+
<span>${escapeHtml(label)}</span>
|
|
229
250
|
</div>
|
|
230
251
|
<ul class="help-index__topics">
|
|
231
252
|
${topics.slice(0, 3).map(topic => `
|
|
232
|
-
<li class="help-index__topic" data-topic="${topic.id}">
|
|
233
|
-
${topic.title}
|
|
253
|
+
<li class="help-index__topic" data-topic="${escapeHtml(topic.id)}">
|
|
254
|
+
${escapeHtml(topic.title)}
|
|
234
255
|
</li>
|
|
235
256
|
`).join('')}
|
|
236
257
|
${topics.length > 3 ? `<li class="help-index__more">+${topics.length - 3} more</li>` : ''}
|
|
@@ -256,6 +277,7 @@ export class HelpModal {
|
|
|
256
277
|
* @private
|
|
257
278
|
*/
|
|
258
279
|
async _loadTopic(topicId) {
|
|
280
|
+
this._disposeToc();
|
|
259
281
|
const content = this._modal.querySelector('.help-modal__content');
|
|
260
282
|
content.innerHTML = '<div class="help-content__loading">Loading...</div>';
|
|
261
283
|
|
|
@@ -265,9 +287,23 @@ export class HelpModal {
|
|
|
265
287
|
});
|
|
266
288
|
|
|
267
289
|
try {
|
|
268
|
-
const topic = await this._service.loadTopic(topicId)
|
|
290
|
+
const topic = (await this._service.loadTopic(topicId))
|
|
291
|
+
?? (this._fallback?.id === topicId ? this._fallback.topic : null);
|
|
292
|
+
// Closed (or closed and opened again) while the topic loaded.
|
|
293
|
+
if (!this._modal?.contains(content)) return;
|
|
269
294
|
this._currentTopic = topicId;
|
|
270
295
|
|
|
296
|
+
if (!topic) {
|
|
297
|
+
// An id nobody provides is not a load failure: say so plainly.
|
|
298
|
+
content.innerHTML = `
|
|
299
|
+
<div class="help-content__error help-content__error--not-found">
|
|
300
|
+
<span class="material-symbols-outlined">search_off</span>
|
|
301
|
+
<p>Help topic not found.</p>
|
|
302
|
+
</div>
|
|
303
|
+
`;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
271
307
|
content.innerHTML = `
|
|
272
308
|
<div class="help-content__article">
|
|
273
309
|
<div class="help-content__breadcrumb">
|
|
@@ -275,10 +311,11 @@ export class HelpModal {
|
|
|
275
311
|
<span class="material-symbols-outlined">home</span>
|
|
276
312
|
</a>
|
|
277
313
|
<span class="help-breadcrumb__separator">/</span>
|
|
278
|
-
<span class="help-breadcrumb__title">${topic.title}</span>
|
|
314
|
+
<span class="help-breadcrumb__title">${escapeHtml(topic.title)}</span>
|
|
279
315
|
</div>
|
|
316
|
+
<div class="help-content__toc" hidden></div>
|
|
280
317
|
<article class="help-article">
|
|
281
|
-
${topic
|
|
318
|
+
${this._articleHtml(topic)}
|
|
282
319
|
</article>
|
|
283
320
|
</div>
|
|
284
321
|
`;
|
|
@@ -288,6 +325,8 @@ export class HelpModal {
|
|
|
288
325
|
e.preventDefault();
|
|
289
326
|
this._showIndex();
|
|
290
327
|
});
|
|
328
|
+
|
|
329
|
+
this._buildToc(content);
|
|
291
330
|
} catch (error) {
|
|
292
331
|
content.innerHTML = `
|
|
293
332
|
<div class="help-content__error">
|
|
@@ -298,6 +337,54 @@ export class HelpModal {
|
|
|
298
337
|
}
|
|
299
338
|
}
|
|
300
339
|
|
|
340
|
+
/**
|
|
341
|
+
* The article body: a markdown topic rendered safe, else the provider's
|
|
342
|
+
* own HTML, which is trusted by contract.
|
|
343
|
+
* @private
|
|
344
|
+
*/
|
|
345
|
+
_articleHtml(topic) {
|
|
346
|
+
if (typeof topic.markdown === 'string') {
|
|
347
|
+
return renderMarkdown(topic.markdown, this._service.markdownOptions ?? {});
|
|
348
|
+
}
|
|
349
|
+
return topic.html ?? '';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The topic's table of contents: one entry per heading in the article, in
|
|
354
|
+
* document order, its level the heading's. Built only when the article
|
|
355
|
+
* has `TOC_MIN_HEADINGS` or more; the outline resolves each entry to its
|
|
356
|
+
* heading ELEMENT, so the provider's markup needs no ids and the modal
|
|
357
|
+
* adds none.
|
|
358
|
+
* @param {HTMLElement} content - The scrolling content pane.
|
|
359
|
+
* @private
|
|
360
|
+
*/
|
|
361
|
+
_buildToc(content) {
|
|
362
|
+
this._disposeToc();
|
|
363
|
+
const host = content.querySelector('.help-content__toc');
|
|
364
|
+
const headings = [...content.querySelectorAll(`.help-article :is(${TOC_HEADINGS})`)];
|
|
365
|
+
if (!host || headings.length < TOC_MIN_HEADINGS) return;
|
|
366
|
+
const byId = new Map(headings.map((heading, index) => [`heading-${index}`, heading]));
|
|
367
|
+
const entries = [...byId].map(([id, heading]) => ({
|
|
368
|
+
id,
|
|
369
|
+
level: Number(heading.tagName.slice(1)),
|
|
370
|
+
label: heading.textContent.trim(),
|
|
371
|
+
}));
|
|
372
|
+
host.hidden = false;
|
|
373
|
+
this._toc = createDocumentOutline(host, {
|
|
374
|
+
entries,
|
|
375
|
+
numbering: true,
|
|
376
|
+
scrollContainer: content,
|
|
377
|
+
resolveTarget: (entry) => byId.get(entry.id) ?? null,
|
|
378
|
+
label: 'Contents',
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** @private */
|
|
383
|
+
_disposeToc() {
|
|
384
|
+
this._toc?.dispose();
|
|
385
|
+
this._toc = null;
|
|
386
|
+
}
|
|
387
|
+
|
|
301
388
|
/**
|
|
302
389
|
* Handle search input.
|
|
303
390
|
* @param {string} query - Search query
|
|
@@ -312,13 +399,14 @@ export class HelpModal {
|
|
|
312
399
|
return;
|
|
313
400
|
}
|
|
314
401
|
|
|
402
|
+
this._disposeToc();
|
|
315
403
|
const results = this._service.searchTopics(query);
|
|
316
404
|
|
|
317
405
|
if (results.length === 0) {
|
|
318
406
|
content.innerHTML = `
|
|
319
407
|
<div class="help-search-results">
|
|
320
408
|
<h2>Search Results</h2>
|
|
321
|
-
<p class="help-search-empty">No results found for "${query}"</p>
|
|
409
|
+
<p class="help-search-empty">No results found for "${escapeHtml(query)}"</p>
|
|
322
410
|
</div>
|
|
323
411
|
`;
|
|
324
412
|
return;
|
|
@@ -327,12 +415,12 @@ export class HelpModal {
|
|
|
327
415
|
content.innerHTML = `
|
|
328
416
|
<div class="help-search-results">
|
|
329
417
|
<h2>Search Results</h2>
|
|
330
|
-
<p class="help-search-count">${results.length} result${results.length !== 1 ? 's' : ''} for "${query}"</p>
|
|
418
|
+
<p class="help-search-count">${results.length} result${results.length !== 1 ? 's' : ''} for "${escapeHtml(query)}"</p>
|
|
331
419
|
<ul class="help-search-list">
|
|
332
420
|
${results.map(topic => `
|
|
333
|
-
<li class="help-search-item" data-topic="${topic.id}">
|
|
334
|
-
<span class="help-search-item__title">${topic.title}</span>
|
|
335
|
-
<span class="help-search-item__category">${this._categories[topic.category]?.label || topic.category}</span>
|
|
421
|
+
<li class="help-search-item" data-topic="${escapeHtml(topic.id)}">
|
|
422
|
+
<span class="help-search-item__title">${escapeHtml(topic.title)}</span>
|
|
423
|
+
<span class="help-search-item__category">${escapeHtml(this._categories[topic.category]?.label || topic.category)}</span>
|
|
336
424
|
</li>
|
|
337
425
|
`).join('')}
|
|
338
426
|
</ul>
|
|
@@ -361,14 +449,28 @@ export class HelpModal {
|
|
|
361
449
|
/**
|
|
362
450
|
* Static method to open help modal.
|
|
363
451
|
* @param {string} [topicId] - Initial topic
|
|
452
|
+
* @param {Object} [options] - As for the instance `open`.
|
|
364
453
|
* @returns {HelpModal}
|
|
365
454
|
*/
|
|
366
|
-
static open(topicId = null) {
|
|
455
|
+
static open(topicId = null, options = {}) {
|
|
367
456
|
const modal = new HelpModal();
|
|
368
|
-
modal.open(topicId);
|
|
457
|
+
modal.open(topicId, options);
|
|
369
458
|
return modal;
|
|
370
459
|
}
|
|
371
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Close the open help modal, or open the index when none is open — what
|
|
463
|
+
* the `?` shortcut does.
|
|
464
|
+
* @returns {HelpModal|null} the modal opened, or null when one was closed
|
|
465
|
+
*/
|
|
466
|
+
static toggle() {
|
|
467
|
+
if (_activeModal) {
|
|
468
|
+
_activeModal.close();
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
return HelpModal.open();
|
|
472
|
+
}
|
|
473
|
+
|
|
372
474
|
/**
|
|
373
475
|
* Get the currently active modal.
|
|
374
476
|
* @returns {HelpModal|null}
|
|
@@ -379,31 +481,55 @@ export class HelpModal {
|
|
|
379
481
|
}
|
|
380
482
|
|
|
381
483
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
382
|
-
//
|
|
484
|
+
// Helpers
|
|
383
485
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
384
486
|
|
|
385
487
|
/**
|
|
386
|
-
*
|
|
488
|
+
* Text for an innerHTML template. Everything the modal interpolates — the
|
|
489
|
+
* search query the user typed, and the topic ids, titles and categories the
|
|
490
|
+
* provider hands back — is TEXT; only `topic.html` is markup, by contract
|
|
491
|
+
* (and `topic.markdown` becomes markup through `renderMarkdown`).
|
|
492
|
+
* Quotes are escaped too, because ids and categories land in attributes.
|
|
387
493
|
*/
|
|
388
|
-
function
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (_activeModal) {
|
|
397
|
-
_activeModal.close();
|
|
398
|
-
} else {
|
|
399
|
-
HelpModal.open();
|
|
400
|
-
}
|
|
401
|
-
});
|
|
494
|
+
function escapeHtml(value) {
|
|
495
|
+
if (value === null || value === undefined) return '';
|
|
496
|
+
return String(value)
|
|
497
|
+
.replace(/&/g, '&')
|
|
498
|
+
.replace(/</g, '<')
|
|
499
|
+
.replace(/>/g, '>')
|
|
500
|
+
.replace(/"/g, '"')
|
|
501
|
+
.replace(/'/g, ''');
|
|
402
502
|
}
|
|
403
503
|
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
504
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
505
|
+
// The `?` shortcut — installed, never imported
|
|
506
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Bind `?` (outside editable fields) to `HelpModal.toggle()` on `target`, and
|
|
510
|
+
* return the disposer that takes it off again.
|
|
511
|
+
*
|
|
512
|
+
* Importing this module binds nothing. It used to add a `document` listener as
|
|
513
|
+
* a side effect of the import, which nobody could remove: a second shell in the
|
|
514
|
+
* same page, or an application with its own `?`, got two handlers. A
|
|
515
|
+
* `createShell()` shell binds `?` through its keymap (`installKeymap`), and
|
|
516
|
+
* `shell.dispose()` takes it off; this is for a page that uses the help modal
|
|
517
|
+
* without the shell.
|
|
518
|
+
*
|
|
519
|
+
* @param {object} [opts]
|
|
520
|
+
* @param {EventTarget} [opts.target=document]
|
|
521
|
+
* @returns {() => void}
|
|
522
|
+
*/
|
|
523
|
+
export function installHelpShortcut({ target = document } = {}) {
|
|
524
|
+
const onKeyDown = (event) => {
|
|
525
|
+
if (event.defaultPrevented || event.key !== '?') return;
|
|
526
|
+
// Don't hijack `?` typed into a field (search boxes, editors).
|
|
527
|
+
if (event.target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
|
|
528
|
+
event.preventDefault();
|
|
529
|
+
HelpModal.toggle();
|
|
530
|
+
};
|
|
531
|
+
target.addEventListener('keydown', onKeyDown);
|
|
532
|
+
return () => target.removeEventListener('keydown', onKeyDown);
|
|
407
533
|
}
|
|
408
534
|
|
|
409
535
|
export default HelpModal;
|
|
@@ -13,8 +13,19 @@
|
|
|
13
13
|
* @typedef {Object} HelpProvider
|
|
14
14
|
* @property {() => string[]} getCategories
|
|
15
15
|
* @property {(category: string) => Array<{id: string, title: string}>} getTopicsByCategory
|
|
16
|
-
* @property {(id: string) => Promise<
|
|
16
|
+
* @property {(id: string) => Promise<HelpTopic|null>} loadTopic
|
|
17
17
|
* @property {(query: string) => Array<Object>} searchTopics
|
|
18
|
+
* @property {Object} [markdownOptions] What the modal passes to
|
|
19
|
+
* `renderMarkdown` for a markdown topic: `highlight`, `math`, `allowHtml`,
|
|
20
|
+
* `inlineRules`, `blockRules`.
|
|
21
|
+
*
|
|
22
|
+
* @typedef {Object} HelpTopic
|
|
23
|
+
* @property {string} title Text.
|
|
24
|
+
* @property {string} [markdown] The article as markdown, rendered safe by
|
|
25
|
+
* `renderMarkdown`: raw HTML is escaped and only http(s), mailto and #anchor
|
|
26
|
+
* links go anywhere. Used when present.
|
|
27
|
+
* @property {string} [html] The article as pre-rendered, TRUSTED HTML,
|
|
28
|
+
* inserted verbatim. Never put unsanitised user content here.
|
|
18
29
|
*
|
|
19
30
|
* @typedef {Object.<string, {label: string, icon: string}>} HelpCategories
|
|
20
31
|
* Keyed by category id; ITERATION ORDER IS DISPLAY ORDER.
|
|
File without changes
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file_picker.js — let the user choose a file to open, and read it.
|
|
3
|
+
*
|
|
4
|
+
* A native window (pywebview, Photino) offers a real open dialog through the
|
|
5
|
+
* optional `host.dialogs.openFile` capability; a browser tab has none. pickFile()
|
|
6
|
+
* asks the host first and, when the host has no `openFile` or answers null
|
|
7
|
+
* ("no dialog behind it", per host.js), opens the browser's own file chooser
|
|
8
|
+
* through a transient `<input type="file">`. The caller gets ONE result shape
|
|
9
|
+
* either way:
|
|
10
|
+
*
|
|
11
|
+
* {ok: true, name, path?, text} encoding 'text' (the default)
|
|
12
|
+
* {ok: true, name, path?, data} encoding 'base64': the file's bytes, base64
|
|
13
|
+
* {ok: false, cancelled: true} the user closed the chooser: say nothing
|
|
14
|
+
* {ok: false, error} show it to the user
|
|
15
|
+
*
|
|
16
|
+
* `path` comes only from a native host; a browser never reveals one.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const ENCODINGS = new Set(['text', 'base64']);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `accept` as a list of extensions or MIME types: an array is kept, a string
|
|
23
|
+
* is split on commas (the `<input accept>` form), each entry trimmed and the
|
|
24
|
+
* empty ones dropped. `'.json, .csv'` → `['.json', '.csv']`.
|
|
25
|
+
*/
|
|
26
|
+
function acceptList(accept) {
|
|
27
|
+
const parts = Array.isArray(accept) ? accept : (accept ? String(accept).split(',') : []);
|
|
28
|
+
return parts.map((a) => String(a).trim()).filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `accept` as the `<input accept>` attribute wants it: comma-separated. */
|
|
32
|
+
function acceptAttr(accept) {
|
|
33
|
+
return acceptList(accept).join(',');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Read `file` as text or as base64, with the FileReader of the file's own window. */
|
|
37
|
+
function readFile(file, encoding, FileReaderCtor) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const reader = new FileReaderCtor();
|
|
40
|
+
reader.onload = () => {
|
|
41
|
+
const name = file.name;
|
|
42
|
+
if (encoding === 'base64') {
|
|
43
|
+
const url = String(reader.result ?? '');
|
|
44
|
+
resolve({ ok: true, name, data: url.slice(url.indexOf(',') + 1) });
|
|
45
|
+
} else {
|
|
46
|
+
resolve({ ok: true, name, text: String(reader.result ?? '') });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
reader.onerror = () => resolve({ ok: false, error: String(reader.error?.message || reader.error || 'The file could not be read.') });
|
|
50
|
+
if (encoding === 'base64') reader.readAsDataURL(file);
|
|
51
|
+
else reader.readAsText(file);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The browser's file chooser: a hidden `<input type="file">`, clicked, removed
|
|
57
|
+
* again once it answers. Cancelling is the input's `cancel` event (every
|
|
58
|
+
* current engine fires it; an engine that does not leaves the promise pending
|
|
59
|
+
* until the user does choose a file).
|
|
60
|
+
*/
|
|
61
|
+
function browserChooser({ accept, encoding, doc }) {
|
|
62
|
+
const FileReaderCtor = doc?.defaultView?.FileReader ?? globalThis.FileReader;
|
|
63
|
+
if (!doc?.body || typeof FileReaderCtor !== 'function') {
|
|
64
|
+
return Promise.resolve({ ok: false, error: 'No file chooser is available here.' });
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const input = doc.createElement('input');
|
|
68
|
+
input.type = 'file';
|
|
69
|
+
const a = acceptAttr(accept);
|
|
70
|
+
if (a) input.accept = a;
|
|
71
|
+
input.style.display = 'none';
|
|
72
|
+
let done = false;
|
|
73
|
+
const finish = (result) => {
|
|
74
|
+
if (done) return;
|
|
75
|
+
done = true;
|
|
76
|
+
input.removeEventListener('change', onChange);
|
|
77
|
+
input.removeEventListener('cancel', onCancel);
|
|
78
|
+
input.remove();
|
|
79
|
+
resolve(result);
|
|
80
|
+
};
|
|
81
|
+
const onChange = () => {
|
|
82
|
+
const file = input.files && input.files[0];
|
|
83
|
+
if (!file) { finish({ ok: false, cancelled: true }); return; }
|
|
84
|
+
readFile(file, encoding, FileReaderCtor).then(finish);
|
|
85
|
+
};
|
|
86
|
+
const onCancel = () => finish({ ok: false, cancelled: true });
|
|
87
|
+
input.addEventListener('change', onChange);
|
|
88
|
+
input.addEventListener('cancel', onCancel);
|
|
89
|
+
doc.body.appendChild(input);
|
|
90
|
+
input.click();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Let the user choose one file and read it.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} host a Host; its optional `dialogs.openFile` is asked first
|
|
98
|
+
* @param {object} [opts]
|
|
99
|
+
* @param {string[]|string} [opts.accept] extensions or MIME types, e.g. ['.json', '.csv'];
|
|
100
|
+
* a string is comma-separated ('.json,.csv'). The
|
|
101
|
+
* native host always receives the list.
|
|
102
|
+
* @param {string|null} [opts.kind] a label for the native dialog's filter (e.g. 'json')
|
|
103
|
+
* @param {'text'|'base64'} [opts.encoding='text'] text, or the bytes as base64
|
|
104
|
+
* @param {Document} [opts.document] where the browser chooser goes (default: the page's)
|
|
105
|
+
* @returns {Promise<{ok: true, name: string, path?: string, text?: string, data?: string}
|
|
106
|
+
* | {ok: false, cancelled: true} | {ok: false, error: string}>}
|
|
107
|
+
*/
|
|
108
|
+
export async function pickFile(host, { accept = [], kind = null, encoding = 'text', document: doc } = {}) {
|
|
109
|
+
if (!ENCODINGS.has(encoding)) throw new TypeError(`pickFile: unknown encoding '${encoding}' (use 'text' or 'base64')`);
|
|
110
|
+
if (typeof host?.dialogs?.openFile === 'function') {
|
|
111
|
+
let r = null;
|
|
112
|
+
try { r = await host.dialogs.openFile({ accept: acceptList(accept), kind, encoding }); }
|
|
113
|
+
catch { r = null; } // a failing host degrades like an absent one
|
|
114
|
+
if (r && typeof r === 'object') return r;
|
|
115
|
+
}
|
|
116
|
+
return browserChooser({ accept, encoding, doc: doc ?? (typeof document !== 'undefined' ? document : null) });
|
|
117
|
+
}
|
package/src/host/host.js
CHANGED
|
@@ -22,11 +22,19 @@ export const HOST_CONTRACT = Object.freeze({
|
|
|
22
22
|
// `chrome()` answers 'native' or 'custom', or a promise of one.
|
|
23
23
|
optional: Object.freeze(['startNativeDrag', 'startNativeResize']),
|
|
24
24
|
}),
|
|
25
|
-
|
|
25
|
+
// `openFile({accept, kind, encoding})` answers {ok: true, name, path?, text | data},
|
|
26
|
+
// {ok: false, cancelled: true}, {ok: false, error}, or null when there is
|
|
27
|
+
// no dialog behind it. Optional: pickFile() (file_picker.js) falls back to
|
|
28
|
+
// the browser's file chooser without it.
|
|
29
|
+
dialogs: Object.freeze({ required: Object.freeze(['saveFile']), optional: Object.freeze(['openFile']) }),
|
|
26
30
|
state: Object.freeze({ required: Object.freeze(['read', 'write']), optional: Object.freeze([]) }),
|
|
27
31
|
// `openExternal(url)` hands a web address to the OS's default browser and
|
|
28
32
|
// resolves true when it did. See external_links.js.
|
|
29
33
|
shell: Object.freeze({ required: Object.freeze(['openExternal']), optional: Object.freeze([]) }),
|
|
34
|
+
// `list(kind)` answers the starter entries `[{id, kind, label, description?,
|
|
35
|
+
// fields?}]` for one kind (every kind when `kind` is null), or a promise of
|
|
36
|
+
// them. Backs mountGalleryPicker() when it is given no `entries`.
|
|
37
|
+
gallery: Object.freeze({ required: Object.freeze(['list']), optional: Object.freeze([]) }),
|
|
30
38
|
});
|
|
31
39
|
|
|
32
40
|
/**
|
|
@@ -52,7 +60,7 @@ export function validateCapability(name, cap) {
|
|
|
52
60
|
* Throws at boot on an incomplete capability — a half-wired host is a bug,
|
|
53
61
|
* and a bug at boot beats a silent degrade at click time.
|
|
54
62
|
*
|
|
55
|
-
* @param {{ window?: object, dialogs?: object, state?: object, shell?: object }} caps
|
|
63
|
+
* @param {{ window?: object, dialogs?: object, state?: object, shell?: object, gallery?: object }} caps
|
|
56
64
|
* @returns {Readonly<object>} the Host
|
|
57
65
|
*/
|
|
58
66
|
export function createHost(caps = {}) {
|
package/src/host/photino_host.js
CHANGED
|
@@ -182,6 +182,15 @@ export function createPhotinoHost(opts = {}) {
|
|
|
182
182
|
catch (e) { warn('dialogs', 'saveFile', e); }
|
|
183
183
|
return r && typeof r === 'object' ? r : null;
|
|
184
184
|
},
|
|
185
|
+
// `{ok: true, name, path?, text | data}` | `{ok: false, cancelled: true}` |
|
|
186
|
+
// `{ok: false, error}`, or null when the host refused: pickFile() then
|
|
187
|
+
// opens the browser's chooser. No timeout, for the same reason as saveFile.
|
|
188
|
+
async openFile({ accept = [], kind = null, encoding = 'text' } = {}) {
|
|
189
|
+
let r = null;
|
|
190
|
+
try { r = await request('dialogs', 'openFile', [{ accept, kind, encoding }], { wait: 0 }); }
|
|
191
|
+
catch (e) { warn('dialogs', 'openFile', e); }
|
|
192
|
+
return r && typeof r === 'object' ? r : null;
|
|
193
|
+
},
|
|
185
194
|
},
|
|
186
195
|
shell: {
|
|
187
196
|
// True when the native side opened it in the system browser; false
|
package/src/host/photino_host.md
CHANGED
|
@@ -14,9 +14,10 @@ host → page {"event": "window.maximizedChanged", "value": true}
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
- Every request gets exactly one reply with the same `id`. A request that gets
|
|
17
|
-
none is given up after 10 s (`timeoutMs`). `dialogs.saveFile`
|
|
18
|
-
|
|
19
|
-
open as long as the user takes. Always
|
|
17
|
+
none is given up after 10 s (`timeoutMs`). `dialogs.saveFile` and
|
|
18
|
+
`dialogs.openFile` are the exceptions: the page waits for them without a
|
|
19
|
+
timeout, because the dialog stays open as long as the user takes. Always
|
|
20
|
+
answer them.
|
|
20
21
|
- `ok: false` means "I can't": the adapter logs it and degrades, exactly as it
|
|
21
22
|
does with pywebview. Use it for unknown ops and for failures.
|
|
22
23
|
- A message with no `id` and an `event` name is an event. The adapter hands it
|
|
@@ -40,6 +41,7 @@ host → page {"event": "window.maximizedChanged", "value": true}
|
|
|
40
41
|
| `window.startDrag` | `[]` | anything, **after mouse-up** | custom chrome only: the OS move loop (below) |
|
|
41
42
|
| `window.startResize` | `[edge]`: `n` `s` `e` `w` `ne` `nw` `se` `sw` | anything, **after mouse-up** | custom chrome only: the OS size loop (below) |
|
|
42
43
|
| `dialogs.saveFile` | `[{data, filename, kind}]` | see below | `ShowSaveFile(...)` + write |
|
|
44
|
+
| `dialogs.openFile` | `[{accept, kind, encoding}]` | see below | `ShowOpenFile(...)` + read |
|
|
43
45
|
| `shell.openExternal` | `[url]` | `true` when opened | the OS's default browser (below) |
|
|
44
46
|
| `state.read` | `[key]` | the stored value, or `null` | a file per key |
|
|
45
47
|
| `state.write` | `[key, value]` | `true` | a file per key |
|
|
@@ -113,6 +115,22 @@ as the reply's `value` (the envelope's own `ok` stays `true`). A reply with the
|
|
|
113
115
|
envelope's `ok: false`, or none at all, makes `saveFile` resolve `null`, and the
|
|
114
116
|
caller downloads the file the browser way instead.
|
|
115
117
|
|
|
118
|
+
**`dialogs.openFile`** (0.17.0, optional). `accept` is a list of extensions or
|
|
119
|
+
MIME types to filter by (`[".json"]`, may be empty), `kind` a label for the
|
|
120
|
+
filter or `null`, and `encoding` `"text"` or `"base64"`. Show an open dialog,
|
|
121
|
+
read the chosen file, and reply with one of:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
{"ok": true, "name": "model.json", "path": "C:\\Users\\me\\model.json", "text": "{...}"} encoding "text": UTF-8
|
|
125
|
+
{"ok": true, "name": "pic.png", "path": "C:\\Users\\me\\pic.png", "data": "iVBO..."} encoding "base64": the bytes
|
|
126
|
+
{"ok": false, "cancelled": true}
|
|
127
|
+
{"ok": false, "error": "The file is in use."}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
as the reply's `value`. A reply with the envelope's `ok: false` (an older
|
|
131
|
+
native side without the op) makes `openFile` resolve `null`, and `pickFile()`
|
|
132
|
+
opens the browser's file chooser instead.
|
|
133
|
+
|
|
116
134
|
**`state`.** `value` is any JSON value; store it and give it back unchanged. A
|
|
117
135
|
string value that holds JSON is parsed on the way back, for hosts that store
|
|
118
136
|
the text.
|
|
@@ -115,6 +115,16 @@ export function createPywebviewHost({ bridgeRef = null, resolvePath = (k) => `${
|
|
|
115
115
|
// Blob download, never report a failed save. Covered by
|
|
116
116
|
// tests/ui/test_host_save_fallback.mjs.
|
|
117
117
|
saveFile: ({ data, filename, kind }) => call('save_file_dialog', [data, filename, kind]),
|
|
118
|
+
// POSITIONAL on the wire like save_file_dialog: read_file_dialog(accept,
|
|
119
|
+
// kind, encoding), where `accept` is a list of extensions or MIME types
|
|
120
|
+
// (may be empty), `kind` a label for the filter or null, and `encoding`
|
|
121
|
+
// 'text' or 'base64'. The reply is openFile's result shape (host.js);
|
|
122
|
+
// null when the transport has no read_file_dialog, and pickFile() then
|
|
123
|
+
// opens the browser's file chooser instead. See the README host contract.
|
|
124
|
+
async openFile({ accept = [], kind = null, encoding = 'text' } = {}) {
|
|
125
|
+
const r = await call('read_file_dialog', [accept, kind, encoding]);
|
|
126
|
+
return r && typeof r === 'object' ? r : null;
|
|
127
|
+
},
|
|
118
128
|
},
|
|
119
129
|
state: {
|
|
120
130
|
async read(key) {
|