flexdesk 0.16.1 → 0.17.1
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 +112 -0
- package/LICENSE +0 -0
- package/README.md +2435 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/canvas.js +18 -0
- package/charts.js +21 -0
- package/core.js +14 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3165 -159
- 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 +707 -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-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
- package/dist/chunk-LWV23YVE.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
- package/dist/chunk-RMTRWWEO.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 +3165 -159
- 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 +3290 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/editor.js +20 -0
- package/flow.js +21 -0
- package/host.js +14 -0
- package/package.json +12 -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 +293 -0
- package/src/ui/components/cell_editors.js +480 -0
- package/src/ui/components/cell_grid.js +761 -0
- package/src/ui/components/cell_paste.js +243 -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 +696 -71
- 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 +361 -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/tsv.js +74 -0
- 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/tiles.js +27 -0
- package/widgets.js +69 -0
- package/wm.js +28 -0
- 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
|
@@ -0,0 +1,2041 @@
|
|
|
1
|
+
// src/help/help_registry.js
|
|
2
|
+
var EMPTY_PROVIDER = Object.freeze({
|
|
3
|
+
getCategories: () => [],
|
|
4
|
+
getTopicsByCategory: () => [],
|
|
5
|
+
loadTopic: async () => null,
|
|
6
|
+
searchTopics: () => []
|
|
7
|
+
});
|
|
8
|
+
var DEFAULT_COPY = Object.freeze({
|
|
9
|
+
title: "Help",
|
|
10
|
+
welcome: "Select a topic from the sidebar or browse the categories below."
|
|
11
|
+
});
|
|
12
|
+
var _provider = EMPTY_PROVIDER;
|
|
13
|
+
var _categories = Object.freeze({});
|
|
14
|
+
var _copy = DEFAULT_COPY;
|
|
15
|
+
function setHelpProvider(provider, categories = {}, copy = {}) {
|
|
16
|
+
_provider = provider ?? EMPTY_PROVIDER;
|
|
17
|
+
_categories = Object.freeze({ ...categories });
|
|
18
|
+
_copy = Object.freeze({ ...DEFAULT_COPY, ...copy });
|
|
19
|
+
}
|
|
20
|
+
function helpProvider() {
|
|
21
|
+
return _provider;
|
|
22
|
+
}
|
|
23
|
+
function helpCategories() {
|
|
24
|
+
return _categories;
|
|
25
|
+
}
|
|
26
|
+
function helpCopy() {
|
|
27
|
+
return _copy;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/ui/utils/markdown.js
|
|
31
|
+
var NEVER_ALLOWED = /* @__PURE__ */ new Set([
|
|
32
|
+
"script",
|
|
33
|
+
"style",
|
|
34
|
+
"iframe",
|
|
35
|
+
"frame",
|
|
36
|
+
"frameset",
|
|
37
|
+
"object",
|
|
38
|
+
"embed",
|
|
39
|
+
"applet",
|
|
40
|
+
"template",
|
|
41
|
+
"noscript",
|
|
42
|
+
"textarea",
|
|
43
|
+
"title",
|
|
44
|
+
"xmp",
|
|
45
|
+
"plaintext",
|
|
46
|
+
"noembed",
|
|
47
|
+
"svg",
|
|
48
|
+
"math",
|
|
49
|
+
"link",
|
|
50
|
+
"meta",
|
|
51
|
+
"base",
|
|
52
|
+
"form",
|
|
53
|
+
"input",
|
|
54
|
+
"button",
|
|
55
|
+
"select",
|
|
56
|
+
"option",
|
|
57
|
+
"video",
|
|
58
|
+
"audio",
|
|
59
|
+
"source",
|
|
60
|
+
"track",
|
|
61
|
+
"img",
|
|
62
|
+
"image",
|
|
63
|
+
"html",
|
|
64
|
+
"head",
|
|
65
|
+
"body",
|
|
66
|
+
"portal",
|
|
67
|
+
"dialog"
|
|
68
|
+
]);
|
|
69
|
+
var VOID_TAGS = /* @__PURE__ */ new Set(["br", "hr", "wbr"]);
|
|
70
|
+
var LINK_URL = /^(?:https?:\/\/|mailto:|#)/i;
|
|
71
|
+
var IMAGE_URL = /^(?:https?:\/\/|\/\/|[^:/?#]*(?:[/?#]|$))/i;
|
|
72
|
+
var ENTITY = /^&(?:[a-zA-Z][a-zA-Z0-9]{1,31}|#\d{1,7}|#[xX][0-9a-fA-F]{1,6});/;
|
|
73
|
+
var ASCII_PUNCT = /[!-/:-@[-`{-~]/;
|
|
74
|
+
var RE = {
|
|
75
|
+
blank: /^[ \t]*$/,
|
|
76
|
+
heading: /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/,
|
|
77
|
+
hr: /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/,
|
|
78
|
+
fence: /^( {0,3})(`{3,}|~{3,})[ \t]*([^\s`]*)[^`]*$/,
|
|
79
|
+
quote: /^ {0,3}> ?/,
|
|
80
|
+
item: /^( {0,3})([-*+]|\d{1,9}[.)])(?=[ \t]|$)([ \t]*)(.*)$/,
|
|
81
|
+
mathOpen: /^ {0,3}\$\$/,
|
|
82
|
+
tableDelim: /^[ \t]*\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/
|
|
83
|
+
};
|
|
84
|
+
function escapeHtml(value) {
|
|
85
|
+
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
86
|
+
}
|
|
87
|
+
function katexMath(tex, { display }) {
|
|
88
|
+
const katex = globalThis.window?.katex ?? globalThis.katex;
|
|
89
|
+
if (typeof katex?.renderToString !== "function") return null;
|
|
90
|
+
try {
|
|
91
|
+
return katex.renderToString(tex, { displayMode: display, throwOnError: false });
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function compileOptions(options) {
|
|
97
|
+
const allow = /* @__PURE__ */ new Set();
|
|
98
|
+
for (const raw of options.allowHtml ?? []) {
|
|
99
|
+
const tag = String(raw).toLowerCase();
|
|
100
|
+
if (!/^[a-z][a-z0-9-]*$/.test(tag)) {
|
|
101
|
+
throw new TypeError(`renderMarkdown: allowHtml entry "${raw}" is not a tag name`);
|
|
102
|
+
}
|
|
103
|
+
if (NEVER_ALLOWED.has(tag)) {
|
|
104
|
+
throw new TypeError(`renderMarkdown: <${tag}> cannot be allowed`);
|
|
105
|
+
}
|
|
106
|
+
allow.add(tag);
|
|
107
|
+
}
|
|
108
|
+
const fn = (value, name) => {
|
|
109
|
+
if (value == null) return null;
|
|
110
|
+
if (typeof value !== "function") throw new TypeError(`renderMarkdown: ${name} must be a function`);
|
|
111
|
+
return value;
|
|
112
|
+
};
|
|
113
|
+
const inlineRules = (options.inlineRules ?? []).map((rule, i) => {
|
|
114
|
+
if (!(rule?.pattern instanceof RegExp) || typeof rule.render !== "function") {
|
|
115
|
+
throw new TypeError(`renderMarkdown: inlineRules[${i}] needs a RegExp pattern and a render function`);
|
|
116
|
+
}
|
|
117
|
+
const flags = rule.pattern.flags.replace(/[gy]/g, "") + "y";
|
|
118
|
+
return { pattern: new RegExp(rule.pattern.source, flags), render: rule.render };
|
|
119
|
+
});
|
|
120
|
+
const blockRules = (options.blockRules ?? []).map((rule, i) => {
|
|
121
|
+
if (!(rule?.start instanceof RegExp) || typeof rule.render !== "function" || rule.end != null && !(rule.end instanceof RegExp)) {
|
|
122
|
+
throw new TypeError(`renderMarkdown: blockRules[${i}] needs a RegExp start, an optional RegExp end and a render function`);
|
|
123
|
+
}
|
|
124
|
+
const plain = (re) => new RegExp(re.source, re.flags.replace(/[gy]/g, ""));
|
|
125
|
+
return { start: plain(rule.start), end: rule.end ? plain(rule.end) : null, render: rule.render };
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
allow,
|
|
129
|
+
highlight: fn(options.highlight, "highlight"),
|
|
130
|
+
math: options.math === void 0 ? katexMath : options.math === false || options.math === null ? null : fn(options.math, "math"),
|
|
131
|
+
inlineRules,
|
|
132
|
+
blockRules
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function findBacktickCloser(text, from, len) {
|
|
136
|
+
let i = from;
|
|
137
|
+
while (i < text.length) {
|
|
138
|
+
const j = text.indexOf("`", i);
|
|
139
|
+
if (j === -1) return -1;
|
|
140
|
+
let k = j;
|
|
141
|
+
while (text[k] === "`") k++;
|
|
142
|
+
if (k - j === len) return j;
|
|
143
|
+
i = k;
|
|
144
|
+
}
|
|
145
|
+
return -1;
|
|
146
|
+
}
|
|
147
|
+
function findBracketClose(text, open) {
|
|
148
|
+
let depth = 0;
|
|
149
|
+
for (let i = open; i < text.length; i++) {
|
|
150
|
+
const c = text[i];
|
|
151
|
+
if (c === "\\") {
|
|
152
|
+
i++;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (c === "`") {
|
|
156
|
+
let k = i;
|
|
157
|
+
while (text[k] === "`") k++;
|
|
158
|
+
const close = findBacktickCloser(text, k, k - i);
|
|
159
|
+
if (close !== -1) {
|
|
160
|
+
i = close + (k - i) - 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
i = k - 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (c === "[") depth++;
|
|
167
|
+
else if (c === "]" && --depth === 0) return i;
|
|
168
|
+
}
|
|
169
|
+
return -1;
|
|
170
|
+
}
|
|
171
|
+
function parseDestination(text, open) {
|
|
172
|
+
if (text[open] !== "(") return null;
|
|
173
|
+
let i = open + 1;
|
|
174
|
+
while (text[i] === " " || text[i] === " " || text[i] === "\n") i++;
|
|
175
|
+
let url = "";
|
|
176
|
+
if (text[i] === "<") {
|
|
177
|
+
const close = text.indexOf(">", i);
|
|
178
|
+
if (close === -1 || /[\n<]/.test(text.slice(i + 1, close))) return null;
|
|
179
|
+
url = text.slice(i + 1, close);
|
|
180
|
+
i = close + 1;
|
|
181
|
+
} else {
|
|
182
|
+
let depth = 0;
|
|
183
|
+
const start = i;
|
|
184
|
+
for (; i < text.length; i++) {
|
|
185
|
+
const c = text[i];
|
|
186
|
+
if (c === "\\" && i + 1 < text.length) {
|
|
187
|
+
i++;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (/\s/.test(c)) break;
|
|
191
|
+
if (c === "(") depth++;
|
|
192
|
+
else if (c === ")") {
|
|
193
|
+
if (depth === 0) break;
|
|
194
|
+
depth--;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
url = text.slice(start, i);
|
|
198
|
+
}
|
|
199
|
+
while (text[i] === " " || text[i] === " " || text[i] === "\n") i++;
|
|
200
|
+
let title = null;
|
|
201
|
+
const q = text[i];
|
|
202
|
+
if (q === '"' || q === "'" || q === "(") {
|
|
203
|
+
const closeQ = q === "(" ? ")" : q;
|
|
204
|
+
const close = text.indexOf(closeQ, i + 1);
|
|
205
|
+
if (close === -1) return null;
|
|
206
|
+
title = text.slice(i + 1, close);
|
|
207
|
+
i = close + 1;
|
|
208
|
+
while (text[i] === " " || text[i] === " " || text[i] === "\n") i++;
|
|
209
|
+
}
|
|
210
|
+
if (text[i] !== ")") return null;
|
|
211
|
+
return { url: url.replace(/\\([!-/:-@[-`{-~])/g, "$1"), title, end: i + 1 };
|
|
212
|
+
}
|
|
213
|
+
var isSpace = (c) => c === void 0 || /\s/.test(c);
|
|
214
|
+
var isWord = (c) => c !== void 0 && /[\p{L}\p{N}]/u.test(c);
|
|
215
|
+
function findEmphasisCloser(text, from, ch, len) {
|
|
216
|
+
let i = from;
|
|
217
|
+
while (i < text.length) {
|
|
218
|
+
const c = text[i];
|
|
219
|
+
if (c === "\\") {
|
|
220
|
+
i += 2;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (c === "`") {
|
|
224
|
+
let k2 = i;
|
|
225
|
+
while (text[k2] === "`") k2++;
|
|
226
|
+
const close = findBacktickCloser(text, k2, k2 - i);
|
|
227
|
+
i = close === -1 ? k2 : close + (k2 - i);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (c !== ch) {
|
|
231
|
+
i++;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
let k = i;
|
|
235
|
+
while (text[k] === ch) k++;
|
|
236
|
+
if (k - i === len && i > from && !isSpace(text[i - 1]) && !(ch === "_" && isWord(text[k]))) return i;
|
|
237
|
+
i = k;
|
|
238
|
+
}
|
|
239
|
+
return -1;
|
|
240
|
+
}
|
|
241
|
+
function parseInlineMath(text, i) {
|
|
242
|
+
if (text[i + 1] === "$") {
|
|
243
|
+
const close = text.indexOf("$$", i + 2);
|
|
244
|
+
if (close === -1 || close === i + 2) return null;
|
|
245
|
+
return { tex: text.slice(i + 2, close), display: true, end: close + 2 };
|
|
246
|
+
}
|
|
247
|
+
if (isSpace(text[i + 1])) return null;
|
|
248
|
+
for (let j = i + 1; j < text.length; j++) {
|
|
249
|
+
if (text[j] === "\\") {
|
|
250
|
+
j++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (text[j] === "\n" && text[j + 1] === "\n") return null;
|
|
254
|
+
if (text[j] === "$" && j > i + 1 && !isSpace(text[j - 1]) && !/\d/.test(text[j + 1] ?? "")) {
|
|
255
|
+
return { tex: text.slice(i + 1, j), display: false, end: j + 1 };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
function renderInline(text, ctx, { inLink = false } = {}) {
|
|
261
|
+
const { opts } = ctx;
|
|
262
|
+
let out = "";
|
|
263
|
+
let plain = "";
|
|
264
|
+
const flush = () => {
|
|
265
|
+
out += escapeHtml(plain);
|
|
266
|
+
plain = "";
|
|
267
|
+
};
|
|
268
|
+
const emit = (html) => {
|
|
269
|
+
flush();
|
|
270
|
+
out += html;
|
|
271
|
+
};
|
|
272
|
+
const open = [];
|
|
273
|
+
let i = 0;
|
|
274
|
+
while (i < text.length) {
|
|
275
|
+
const c = text[i];
|
|
276
|
+
if (c === "\\") {
|
|
277
|
+
const n = text[i + 1];
|
|
278
|
+
if (n === "\n") {
|
|
279
|
+
emit("<br>\n");
|
|
280
|
+
i += 2;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (n !== void 0 && ASCII_PUNCT.test(n)) {
|
|
284
|
+
plain += n;
|
|
285
|
+
i += 2;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
plain += c;
|
|
289
|
+
i++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (c === "`") {
|
|
293
|
+
let k = i;
|
|
294
|
+
while (text[k] === "`") k++;
|
|
295
|
+
const close = findBacktickCloser(text, k, k - i);
|
|
296
|
+
if (close === -1) {
|
|
297
|
+
plain += text.slice(i, k);
|
|
298
|
+
i = k;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
let code = text.slice(k, close).replace(/\n/g, " ");
|
|
302
|
+
if (code.length > 2 && code[0] === " " && code.at(-1) === " " && code.trim()) code = code.slice(1, -1);
|
|
303
|
+
emit(`<code>${escapeHtml(code)}</code>`);
|
|
304
|
+
i = close + (k - i);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
let ruled = false;
|
|
308
|
+
for (const rule of opts.inlineRules) {
|
|
309
|
+
rule.pattern.lastIndex = i;
|
|
310
|
+
const m = rule.pattern.exec(text);
|
|
311
|
+
if (!m || !m[0].length) continue;
|
|
312
|
+
const html = rule.render(m, ctx.api);
|
|
313
|
+
if (html == null) continue;
|
|
314
|
+
emit(String(html));
|
|
315
|
+
i += m[0].length;
|
|
316
|
+
ruled = true;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
if (ruled) continue;
|
|
320
|
+
if (c === "\n") {
|
|
321
|
+
if (/ {2,}$/.test(plain)) {
|
|
322
|
+
plain = plain.replace(/ +$/, "");
|
|
323
|
+
emit("<br>\n");
|
|
324
|
+
} else {
|
|
325
|
+
plain = plain.replace(/ +$/, "");
|
|
326
|
+
plain += "\n";
|
|
327
|
+
}
|
|
328
|
+
i++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (c === "&") {
|
|
332
|
+
const m = ENTITY.exec(text.slice(i, i + 40));
|
|
333
|
+
if (m) {
|
|
334
|
+
emit(m[0]);
|
|
335
|
+
i += m[0].length;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (c === "$" && opts.math) {
|
|
340
|
+
const m = parseInlineMath(text, i);
|
|
341
|
+
if (m) {
|
|
342
|
+
const html = opts.math(m.tex, { display: m.display });
|
|
343
|
+
if (html == null) plain += text.slice(i, m.end);
|
|
344
|
+
else emit(String(html));
|
|
345
|
+
i = m.end;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (c === "!" && text[i + 1] === "[" || c === "[" && !inLink) {
|
|
350
|
+
const bracket = c === "!" ? i + 1 : i;
|
|
351
|
+
const close = findBracketClose(text, bracket);
|
|
352
|
+
const dest = close === -1 ? null : parseDestination(text, close + 1);
|
|
353
|
+
if (dest) {
|
|
354
|
+
const label = text.slice(bracket + 1, close);
|
|
355
|
+
const title = dest.title == null ? "" : ` title="${escapeHtml(dest.title)}"`;
|
|
356
|
+
if (c === "!") {
|
|
357
|
+
if (IMAGE_URL.test(dest.url) && !/[\s\x00-\x1f]/.test(dest.url)) {
|
|
358
|
+
emit(`<img src="${escapeHtml(dest.url)}" alt="${escapeHtml(label)}"${title}>`);
|
|
359
|
+
} else {
|
|
360
|
+
plain += label;
|
|
361
|
+
}
|
|
362
|
+
} else if (LINK_URL.test(dest.url) && !/[\s\x00-\x1f]/.test(dest.url)) {
|
|
363
|
+
emit(`<a href="${escapeHtml(dest.url)}"${title} rel="noopener noreferrer">${renderInline(label, ctx, { inLink: true })}</a>`);
|
|
364
|
+
} else {
|
|
365
|
+
emit(renderInline(label, ctx, { inLink: true }));
|
|
366
|
+
}
|
|
367
|
+
i = dest.end;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (c === "<") {
|
|
372
|
+
const auto = /^<((?:https?:\/\/|mailto:)[^\s<>]+)>/i.exec(text.slice(i, i + 2048));
|
|
373
|
+
if (auto && !inLink) {
|
|
374
|
+
emit(`<a href="${escapeHtml(auto[1])}" rel="noopener noreferrer">${escapeHtml(auto[1])}</a>`);
|
|
375
|
+
i += auto[0].length;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const tag = opts.allow.size ? /^<(\/?)([A-Za-z][A-Za-z0-9-]*)(?:\s[^<>]*)?\/?>/.exec(text.slice(i, i + 1024)) : null;
|
|
379
|
+
const name = tag?.[2].toLowerCase();
|
|
380
|
+
if (tag && opts.allow.has(name)) {
|
|
381
|
+
if (VOID_TAGS.has(name)) {
|
|
382
|
+
if (!tag[1]) emit(`<${name}>`);
|
|
383
|
+
i += tag[0].length;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (!tag[1]) {
|
|
387
|
+
emit(`<${name}>`);
|
|
388
|
+
open.push(name);
|
|
389
|
+
i += tag[0].length;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const at = open.lastIndexOf(name);
|
|
393
|
+
if (at !== -1) {
|
|
394
|
+
while (open.length > at) emit(`</${open.pop()}>`);
|
|
395
|
+
i += tag[0].length;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (c === "*" || c === "_" || c === "~") {
|
|
401
|
+
let k = i;
|
|
402
|
+
while (text[k] === c) k++;
|
|
403
|
+
const run = k - i;
|
|
404
|
+
const opens = !isSpace(text[k]) && !(c === "_" && isWord(text[i - 1]));
|
|
405
|
+
const len = c === "~" ? run === 2 ? 2 : 0 : run <= 3 ? run : 0;
|
|
406
|
+
if (opens && len) {
|
|
407
|
+
const close = findEmphasisCloser(text, k, c, len);
|
|
408
|
+
if (close !== -1) {
|
|
409
|
+
const inner = renderInline(text.slice(k, close), ctx, { inLink });
|
|
410
|
+
const wrapped = c === "~" ? `<del>${inner}</del>` : len === 1 ? `<em>${inner}</em>` : len === 2 ? `<strong>${inner}</strong>` : `<strong><em>${inner}</em></strong>`;
|
|
411
|
+
emit(wrapped);
|
|
412
|
+
i = close + len;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
plain += text.slice(i, k);
|
|
417
|
+
i = k;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
plain += c;
|
|
421
|
+
i++;
|
|
422
|
+
}
|
|
423
|
+
flush();
|
|
424
|
+
while (open.length) out += `</${open.pop()}>`;
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
function splitRow(line) {
|
|
428
|
+
let s = line.trim();
|
|
429
|
+
if (s.startsWith("|")) s = s.slice(1);
|
|
430
|
+
if (s.endsWith("|") && !s.endsWith("\\|")) s = s.slice(0, -1);
|
|
431
|
+
const cells = [];
|
|
432
|
+
let cell = "";
|
|
433
|
+
let inCode = 0;
|
|
434
|
+
for (let i = 0; i < s.length; i++) {
|
|
435
|
+
const c = s[i];
|
|
436
|
+
if (c === "\\" && s[i + 1] === "|") {
|
|
437
|
+
cell += "|";
|
|
438
|
+
i++;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (c === "`") {
|
|
442
|
+
let k = i;
|
|
443
|
+
while (s[k] === "`") k++;
|
|
444
|
+
const len = k - i;
|
|
445
|
+
if (inCode === len) inCode = 0;
|
|
446
|
+
else if (!inCode && findBacktickCloser(s, k, len) !== -1) inCode = len;
|
|
447
|
+
cell += s.slice(i, k);
|
|
448
|
+
i = k - 1;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (c === "|" && !inCode) {
|
|
452
|
+
cells.push(cell.trim());
|
|
453
|
+
cell = "";
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
cell += c;
|
|
457
|
+
}
|
|
458
|
+
cells.push(cell.trim());
|
|
459
|
+
return cells;
|
|
460
|
+
}
|
|
461
|
+
function isTableStart(lines, i) {
|
|
462
|
+
if (!lines[i].includes("|") || i + 1 >= lines.length || !lines[i + 1].includes("|") || !RE.tableDelim.test(lines[i + 1])) return false;
|
|
463
|
+
return splitRow(lines[i]).length === splitRow(lines[i + 1]).length;
|
|
464
|
+
}
|
|
465
|
+
function blockRuleAt(line, ctx) {
|
|
466
|
+
for (const rule of ctx.opts.blockRules) {
|
|
467
|
+
const match = rule.start.exec(line);
|
|
468
|
+
if (match) return { rule, match };
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
function isMathBlockStart(line, ctx) {
|
|
473
|
+
if (!ctx.opts.math || !RE.mathOpen.test(line)) return false;
|
|
474
|
+
const rest = line.replace(RE.mathOpen, "");
|
|
475
|
+
return !rest.includes("$$") || /^[^$]*\$\$[ \t]*$/.test(rest) && !/^[ \t]*\$\$/.test(rest);
|
|
476
|
+
}
|
|
477
|
+
function startsBlock(lines, i, ctx) {
|
|
478
|
+
const line = lines[i];
|
|
479
|
+
if (RE.heading.test(line) || RE.hr.test(line) || RE.fence.test(line) || RE.quote.test(line)) return true;
|
|
480
|
+
if (isMathBlockStart(line, ctx)) return true;
|
|
481
|
+
const item = RE.item.exec(line);
|
|
482
|
+
if (item && item[4].trim() && (!/\d/.test(item[2]) || /^1[.)]$/.test(item[2]))) return true;
|
|
483
|
+
if (blockRuleAt(line, ctx)) return true;
|
|
484
|
+
return isTableStart(lines, i);
|
|
485
|
+
}
|
|
486
|
+
function renderCode(lines, i, ctx) {
|
|
487
|
+
const [, indent, fence, lang] = RE.fence.exec(lines[i]);
|
|
488
|
+
const body = [];
|
|
489
|
+
const close = new RegExp(`^ {0,3}${fence[0] === "`" ? "`" : "~"}{${fence.length},}[ \\t]*$`);
|
|
490
|
+
let j = i + 1;
|
|
491
|
+
for (; j < lines.length; j++) {
|
|
492
|
+
if (close.test(lines[j])) {
|
|
493
|
+
j++;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
let line = lines[j];
|
|
497
|
+
for (let n = 0; n < indent.length && line[0] === " "; n++) line = line.slice(1);
|
|
498
|
+
body.push(line);
|
|
499
|
+
}
|
|
500
|
+
const code = body.length ? body.join("\n") + "\n" : "";
|
|
501
|
+
const highlighted = ctx.opts.highlight ? ctx.opts.highlight(code, lang) : null;
|
|
502
|
+
const cls = lang ? ` class="language-${escapeHtml(lang)}"` : "";
|
|
503
|
+
return { html: `<pre><code${cls}>${highlighted == null ? escapeHtml(code) : String(highlighted)}</code></pre>
|
|
504
|
+
`, next: j };
|
|
505
|
+
}
|
|
506
|
+
function renderMathBlock(lines, i, ctx) {
|
|
507
|
+
const first = lines[i].replace(/^ {0,3}\$\$/, "");
|
|
508
|
+
const body = [];
|
|
509
|
+
let j = i;
|
|
510
|
+
let closed = false;
|
|
511
|
+
if (/\$\$[ \t]*$/.test(first)) {
|
|
512
|
+
body.push(first.replace(/\$\$[ \t]*$/, ""));
|
|
513
|
+
closed = true;
|
|
514
|
+
j = i + 1;
|
|
515
|
+
} else {
|
|
516
|
+
if (first.trim()) body.push(first);
|
|
517
|
+
for (j = i + 1; j < lines.length; j++) {
|
|
518
|
+
if (/\$\$[ \t]*$/.test(lines[j])) {
|
|
519
|
+
const last = lines[j].replace(/\$\$[ \t]*$/, "");
|
|
520
|
+
if (last.trim()) body.push(last);
|
|
521
|
+
closed = true;
|
|
522
|
+
j++;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
body.push(lines[j]);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const tex = body.join("\n").trim();
|
|
529
|
+
const html = closed && tex ? ctx.opts.math(tex, { display: true }) : null;
|
|
530
|
+
if (html != null) return { html: `<div class="twm-md-math">${String(html)}</div>
|
|
531
|
+
`, next: j };
|
|
532
|
+
return { html: `<p>${escapeHtml(lines.slice(i, j).join("\n"))}</p>
|
|
533
|
+
`, next: j };
|
|
534
|
+
}
|
|
535
|
+
function renderTable(lines, i, ctx) {
|
|
536
|
+
const head = splitRow(lines[i]);
|
|
537
|
+
const align = splitRow(lines[i + 1]).map((d) => {
|
|
538
|
+
const l = d.startsWith(":");
|
|
539
|
+
const r = d.endsWith(":");
|
|
540
|
+
return l && r ? "center" : r ? "right" : l ? "left" : null;
|
|
541
|
+
});
|
|
542
|
+
const cell = (tag, text, n) => {
|
|
543
|
+
const style = align[n] ? ` style="text-align: ${align[n]}"` : "";
|
|
544
|
+
return `<${tag}${style}>${renderInline(text ?? "", ctx)}</${tag}>`;
|
|
545
|
+
};
|
|
546
|
+
let html = "<table>\n<thead>\n<tr>" + head.map((t, n) => cell("th", t, n)).join("") + "</tr>\n</thead>\n";
|
|
547
|
+
let j = i + 2;
|
|
548
|
+
const rows = [];
|
|
549
|
+
for (; j < lines.length; j++) {
|
|
550
|
+
if (RE.blank.test(lines[j]) || startsBlock(lines, j, ctx)) break;
|
|
551
|
+
const cells = splitRow(lines[j]);
|
|
552
|
+
rows.push("<tr>" + head.map((_, n) => cell("td", cells[n], n)).join("") + "</tr>\n");
|
|
553
|
+
}
|
|
554
|
+
if (rows.length) html += "<tbody>\n" + rows.join("") + "</tbody>\n";
|
|
555
|
+
return { html: html + "</table>\n", next: j };
|
|
556
|
+
}
|
|
557
|
+
function renderList(lines, i, ctx) {
|
|
558
|
+
const first = RE.item.exec(lines[i]);
|
|
559
|
+
const ordered = /\d/.test(first[2]);
|
|
560
|
+
const delim = ordered ? first[2].at(-1) : first[2];
|
|
561
|
+
const sameList = (m) => m && (ordered ? /\d/.test(m[2]) && m[2].at(-1) === delim : m[2] === delim);
|
|
562
|
+
const items = [];
|
|
563
|
+
let loose = false;
|
|
564
|
+
let j = i;
|
|
565
|
+
while (j < lines.length) {
|
|
566
|
+
const m = RE.item.exec(lines[j]);
|
|
567
|
+
if (!sameList(m) || items.length && m[1].length >= items.at(-1).indent) break;
|
|
568
|
+
const gap = m[3].length >= 5 || !m[4] ? 1 : m[3].length;
|
|
569
|
+
const indent = m[1].length + m[2].length + gap;
|
|
570
|
+
const item = { indent, lines: [m[3].length >= 5 ? " ".repeat(m[3].length - 1) + m[4] : m[4]] };
|
|
571
|
+
items.push(item);
|
|
572
|
+
j++;
|
|
573
|
+
let pendingBlank = 0;
|
|
574
|
+
while (j < lines.length) {
|
|
575
|
+
const line = lines[j];
|
|
576
|
+
if (RE.blank.test(line)) {
|
|
577
|
+
pendingBlank++;
|
|
578
|
+
j++;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const lead = line.match(/^ */)[0].length;
|
|
582
|
+
if (lead >= indent) {
|
|
583
|
+
for (; pendingBlank; pendingBlank--) item.lines.push("");
|
|
584
|
+
item.lines.push(line.slice(indent));
|
|
585
|
+
j++;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const next = RE.item.exec(line);
|
|
589
|
+
if (pendingBlank === 0 && !next && !startsBlock(lines, j, ctx) && !RE.blank.test(item.lines.at(-1) ?? "")) {
|
|
590
|
+
item.lines.push(line.trim());
|
|
591
|
+
j++;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
if (pendingBlank) {
|
|
597
|
+
const next = RE.item.exec(lines[j] ?? "");
|
|
598
|
+
if (j < lines.length && sameList(next) && next[1].length < indent) loose = true;
|
|
599
|
+
else {
|
|
600
|
+
j -= pendingBlank;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (item.lines.slice(0, -1).some((l) => RE.blank.test(l)) && item.lines.length > 1) {
|
|
605
|
+
const inner = item.lines.join("\n");
|
|
606
|
+
if (!/^(`{3,}|~{3,})/m.test(inner)) loose = true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const tag = ordered ? "ol" : "ul";
|
|
610
|
+
const start = ordered ? parseInt(first[2], 10) : 1;
|
|
611
|
+
let html = start !== 1 ? `<${tag} start="${start}">
|
|
612
|
+
` : `<${tag}>
|
|
613
|
+
`;
|
|
614
|
+
for (const item of items) {
|
|
615
|
+
const body = renderBlocks(item.lines, ctx, { tight: !loose });
|
|
616
|
+
html += `<li>${body.replace(/\n$/, "")}</li>
|
|
617
|
+
`;
|
|
618
|
+
}
|
|
619
|
+
return { html: html + `</${tag}>
|
|
620
|
+
`, next: j };
|
|
621
|
+
}
|
|
622
|
+
function renderBlocks(lines, ctx, { tight = false } = {}) {
|
|
623
|
+
let out = "";
|
|
624
|
+
let i = 0;
|
|
625
|
+
while (i < lines.length) {
|
|
626
|
+
const line = lines[i];
|
|
627
|
+
if (RE.blank.test(line)) {
|
|
628
|
+
i++;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const ruled = blockRuleAt(line, ctx);
|
|
632
|
+
if (ruled) {
|
|
633
|
+
let j2 = i + 1;
|
|
634
|
+
let closed = true;
|
|
635
|
+
if (ruled.rule.end) {
|
|
636
|
+
closed = false;
|
|
637
|
+
for (; j2 < lines.length; j2++) {
|
|
638
|
+
if (ruled.rule.end.test(lines[j2])) {
|
|
639
|
+
closed = true;
|
|
640
|
+
j2++;
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const html = ruled.rule.render({ lines: lines.slice(i, j2), match: ruled.match, closed }, ctx.api);
|
|
646
|
+
if (html != null) {
|
|
647
|
+
out += String(html) + "\n";
|
|
648
|
+
i = j2;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
let block = null;
|
|
653
|
+
if (RE.fence.test(line)) block = renderCode(lines, i, ctx);
|
|
654
|
+
else if (isMathBlockStart(line, ctx)) block = renderMathBlock(lines, i, ctx);
|
|
655
|
+
else if (RE.heading.test(line)) {
|
|
656
|
+
const [, hashes, text = ""] = RE.heading.exec(line);
|
|
657
|
+
const n = hashes.length;
|
|
658
|
+
block = { html: `<h${n}>${renderInline(text.trim(), ctx)}</h${n}>
|
|
659
|
+
`, next: i + 1 };
|
|
660
|
+
} else if (RE.hr.test(line)) block = { html: "<hr>\n", next: i + 1 };
|
|
661
|
+
else if (RE.quote.test(line)) {
|
|
662
|
+
const inner = [];
|
|
663
|
+
let j2 = i;
|
|
664
|
+
for (; j2 < lines.length && RE.quote.test(lines[j2]); j2++) inner.push(lines[j2].replace(RE.quote, ""));
|
|
665
|
+
block = { html: `<blockquote>
|
|
666
|
+
${renderBlocks(inner, ctx)}</blockquote>
|
|
667
|
+
`, next: j2 };
|
|
668
|
+
} else if (RE.item.test(line)) block = renderList(lines, i, ctx);
|
|
669
|
+
else if (isTableStart(lines, i)) block = renderTable(lines, i, ctx);
|
|
670
|
+
if (block) {
|
|
671
|
+
out += block.html;
|
|
672
|
+
i = block.next;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const para = [line];
|
|
676
|
+
let j = i + 1;
|
|
677
|
+
for (; j < lines.length && !RE.blank.test(lines[j]) && !startsBlock(lines, j, ctx); j++) para.push(lines[j]);
|
|
678
|
+
const inline = renderInline(para.map((l, n) => n === 0 ? l.replace(/^[ \t]+/, "") : l.trimStart()).join("\n").replace(/[ \t]+$/, ""), ctx);
|
|
679
|
+
out += tight ? `${inline}
|
|
680
|
+
` : `<p>${inline}</p>
|
|
681
|
+
`;
|
|
682
|
+
i = j;
|
|
683
|
+
}
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
686
|
+
function renderMarkdown(source, options = {}) {
|
|
687
|
+
const ctx = { opts: compileOptions(options ?? {}) };
|
|
688
|
+
ctx.api = Object.freeze({
|
|
689
|
+
escape: escapeHtml,
|
|
690
|
+
inline: (text2) => renderInline(String(text2 ?? ""), ctx)
|
|
691
|
+
});
|
|
692
|
+
const text = String(source ?? "").replace(/\r\n?/g, "\n").replace(/\u0000/g, "\uFFFD");
|
|
693
|
+
if (options?.inline) return renderInline(text, ctx);
|
|
694
|
+
return renderBlocks(text.split("\n"), ctx);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/ui/components/document_outline.js
|
|
698
|
+
var DocumentOutline = class {
|
|
699
|
+
/**
|
|
700
|
+
* @param {HTMLElement|null} host appended to, if given
|
|
701
|
+
* @param {DocumentOutlineConfig} [config]
|
|
702
|
+
*/
|
|
703
|
+
constructor(host, config = {}) {
|
|
704
|
+
this.config = {
|
|
705
|
+
entries: [],
|
|
706
|
+
numbering: false,
|
|
707
|
+
maxDepth: 3,
|
|
708
|
+
onSelect: null,
|
|
709
|
+
scrollOnSelect: true,
|
|
710
|
+
scrollBehavior: "smooth",
|
|
711
|
+
scrollContainer: null,
|
|
712
|
+
resolveTarget: null,
|
|
713
|
+
rootMargin: "0px 0px -70% 0px",
|
|
714
|
+
scrollEndTimeout: 1e3,
|
|
715
|
+
emptyState: "No headings",
|
|
716
|
+
label: "Outline",
|
|
717
|
+
className: null,
|
|
718
|
+
IntersectionObserver: null,
|
|
719
|
+
...config
|
|
720
|
+
};
|
|
721
|
+
this._entries = [];
|
|
722
|
+
this._rows = [];
|
|
723
|
+
this._byId = /* @__PURE__ */ new Map();
|
|
724
|
+
this._byTarget = /* @__PURE__ */ new Map();
|
|
725
|
+
this._inView = /* @__PURE__ */ new Set();
|
|
726
|
+
this._activeId = null;
|
|
727
|
+
this._listeners = /* @__PURE__ */ new Set();
|
|
728
|
+
this._observer = null;
|
|
729
|
+
this._suppressed = false;
|
|
730
|
+
this._timer = null;
|
|
731
|
+
this._disposed = false;
|
|
732
|
+
const root = document.createElement("nav");
|
|
733
|
+
root.className = "twm-doc-outline" + (this.config.className ? ` ${this.config.className}` : "");
|
|
734
|
+
if (this.config.label) root.setAttribute("aria-label", this.config.label);
|
|
735
|
+
const list = document.createElement("ol");
|
|
736
|
+
list.className = "twm-doc-outline__items";
|
|
737
|
+
root.appendChild(list);
|
|
738
|
+
this.element = root;
|
|
739
|
+
this.listElement = list;
|
|
740
|
+
this._onClick = (ev) => this._click(ev);
|
|
741
|
+
this._onScrollEnd = () => this._resume();
|
|
742
|
+
list.addEventListener("click", this._onClick);
|
|
743
|
+
this.config.scrollContainer?.addEventListener?.("scrollend", this._onScrollEnd);
|
|
744
|
+
this._setEntries(this.config.entries);
|
|
745
|
+
if (host) host.appendChild(root);
|
|
746
|
+
}
|
|
747
|
+
// ── public ───────────────────────────────────────────────────────────
|
|
748
|
+
/** Replace the entries. The active entry stays active if it is still
|
|
749
|
+
* listed and resolved; otherwise there is none. */
|
|
750
|
+
setEntries(entries) {
|
|
751
|
+
if (this._disposed) return;
|
|
752
|
+
this._setEntries(entries);
|
|
753
|
+
}
|
|
754
|
+
/** The entries, as last given. */
|
|
755
|
+
getEntries() {
|
|
756
|
+
return this._entries.slice();
|
|
757
|
+
}
|
|
758
|
+
/** Resolve every target again and watch the new ones: the document under
|
|
759
|
+
* the outline was re-rendered. */
|
|
760
|
+
refresh() {
|
|
761
|
+
if (this._disposed) return;
|
|
762
|
+
this._observe();
|
|
763
|
+
this._keepActive();
|
|
764
|
+
}
|
|
765
|
+
/** Mark an entry active (`null` for none). An id that is not listed, or
|
|
766
|
+
* whose target is not resolved, is refused: returns false. */
|
|
767
|
+
setActive(id) {
|
|
768
|
+
if (this._disposed) return false;
|
|
769
|
+
if (id == null) {
|
|
770
|
+
this._activate(null);
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
const row = this._byId.get(String(id));
|
|
774
|
+
if (!row?.target) return false;
|
|
775
|
+
this._activate(row.id);
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
/** The active entry's id, or null. */
|
|
779
|
+
getActive() {
|
|
780
|
+
return this._activeId;
|
|
781
|
+
}
|
|
782
|
+
/** `fn(id, entry)` on every change of the active entry (`(null, null)` when
|
|
783
|
+
* none). Returns the function that unsubscribes. */
|
|
784
|
+
onActiveChange(fn) {
|
|
785
|
+
if (typeof fn !== "function" || this._disposed) return () => {
|
|
786
|
+
};
|
|
787
|
+
this._listeners.add(fn);
|
|
788
|
+
return () => this._listeners.delete(fn);
|
|
789
|
+
}
|
|
790
|
+
/** The `<li>` drawn for an id, or null. */
|
|
791
|
+
getItem(id) {
|
|
792
|
+
return this._byId.get(String(id))?.li || null;
|
|
793
|
+
}
|
|
794
|
+
dispose() {
|
|
795
|
+
if (this._disposed) return;
|
|
796
|
+
this._disposed = true;
|
|
797
|
+
this._observer?.disconnect();
|
|
798
|
+
this._observer = null;
|
|
799
|
+
this._clearTimer();
|
|
800
|
+
this._suppressed = false;
|
|
801
|
+
this.config.scrollContainer?.removeEventListener?.("scrollend", this._onScrollEnd);
|
|
802
|
+
this.listElement.removeEventListener("click", this._onClick);
|
|
803
|
+
this._listeners.clear();
|
|
804
|
+
this._byId.clear();
|
|
805
|
+
this._byTarget.clear();
|
|
806
|
+
this._inView.clear();
|
|
807
|
+
this._rows = [];
|
|
808
|
+
this.element.remove();
|
|
809
|
+
}
|
|
810
|
+
// ── drawing ──────────────────────────────────────────────────────────
|
|
811
|
+
_setEntries(entries) {
|
|
812
|
+
this._entries = Array.isArray(entries) ? entries.slice() : [];
|
|
813
|
+
this._rows = numberEntries(this._entries, this.config.maxDepth);
|
|
814
|
+
this._byId = new Map(this._rows.map((row) => [row.id, row]));
|
|
815
|
+
this._draw();
|
|
816
|
+
this._observe();
|
|
817
|
+
this._keepActive();
|
|
818
|
+
}
|
|
819
|
+
/** The active entry survives a redraw only while it is listed and resolved. */
|
|
820
|
+
_keepActive() {
|
|
821
|
+
if (this._activeId !== null && !this._byId.get(this._activeId)?.target) this._activate(null);
|
|
822
|
+
else this._paint();
|
|
823
|
+
}
|
|
824
|
+
_draw() {
|
|
825
|
+
const list = this.listElement;
|
|
826
|
+
list.replaceChildren();
|
|
827
|
+
this.element.querySelector(":scope > .twm-doc-outline__empty")?.remove();
|
|
828
|
+
this.element.classList.toggle("twm-doc-outline--empty", this._rows.length === 0);
|
|
829
|
+
this.element.classList.toggle("twm-doc-outline--numbered", !!this.config.numbering);
|
|
830
|
+
if (!this._rows.length) {
|
|
831
|
+
this._drawEmpty();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
for (const row of this._rows) {
|
|
835
|
+
const li = document.createElement("li");
|
|
836
|
+
li.className = `twm-doc-outline__item twm-doc-outline__item--level-${row.depth}`;
|
|
837
|
+
li.style.setProperty("--twm-doc-outline-depth", String(row.depth - 1));
|
|
838
|
+
const button = document.createElement("button");
|
|
839
|
+
button.type = "button";
|
|
840
|
+
button.className = "twm-doc-outline__link";
|
|
841
|
+
if (this.config.numbering) {
|
|
842
|
+
const num = document.createElement("span");
|
|
843
|
+
num.className = "twm-doc-outline__number";
|
|
844
|
+
num.textContent = row.number;
|
|
845
|
+
button.appendChild(num);
|
|
846
|
+
}
|
|
847
|
+
const label = document.createElement("span");
|
|
848
|
+
label.className = "twm-doc-outline__label";
|
|
849
|
+
label.textContent = String(row.entry.label ?? "");
|
|
850
|
+
button.appendChild(label);
|
|
851
|
+
button.title = (this.config.numbering ? `${row.number} ` : "") + label.textContent;
|
|
852
|
+
li.appendChild(button);
|
|
853
|
+
row.li = li;
|
|
854
|
+
list.appendChild(li);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
_drawEmpty() {
|
|
858
|
+
let content = this.config.emptyState;
|
|
859
|
+
if (typeof content === "function") content = content();
|
|
860
|
+
if (content == null || content === false || content === "") return;
|
|
861
|
+
const p = document.createElement("p");
|
|
862
|
+
p.className = "twm-doc-outline__empty";
|
|
863
|
+
if (typeof Node !== "undefined" && content instanceof Node) p.appendChild(content);
|
|
864
|
+
else p.textContent = String(content);
|
|
865
|
+
this.element.appendChild(p);
|
|
866
|
+
}
|
|
867
|
+
_paint() {
|
|
868
|
+
for (const row of this._rows) {
|
|
869
|
+
if (!row.li) continue;
|
|
870
|
+
const active = row.id === this._activeId;
|
|
871
|
+
row.li.classList.toggle("twm-doc-outline__item--active", active);
|
|
872
|
+
row.li.classList.toggle("twm-doc-outline__item--unresolved", !row.target);
|
|
873
|
+
const button = row.li.firstChild;
|
|
874
|
+
if (active) button.setAttribute("aria-current", "location");
|
|
875
|
+
else button.removeAttribute("aria-current");
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
_activate(id) {
|
|
879
|
+
const next = id ?? null;
|
|
880
|
+
if (next === this._activeId) {
|
|
881
|
+
this._paint();
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
this._activeId = next;
|
|
885
|
+
this._paint();
|
|
886
|
+
const entry = next === null ? null : this._byId.get(next)?.entry ?? null;
|
|
887
|
+
for (const fn of [...this._listeners]) fn(next, entry);
|
|
888
|
+
}
|
|
889
|
+
// ── the scroll-spy ───────────────────────────────────────────────────
|
|
890
|
+
_resolve(entry) {
|
|
891
|
+
const container = this.config.scrollContainer;
|
|
892
|
+
if (typeof this.config.resolveTarget === "function") {
|
|
893
|
+
return this.config.resolveTarget(entry, container) || null;
|
|
894
|
+
}
|
|
895
|
+
if (!container) return null;
|
|
896
|
+
const id = String(entry.id);
|
|
897
|
+
for (const el of container.querySelectorAll("[id]")) if (el.id === id) return el;
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
_observe() {
|
|
901
|
+
this._observer?.disconnect();
|
|
902
|
+
this._byTarget.clear();
|
|
903
|
+
this._inView.clear();
|
|
904
|
+
for (const row of this._rows) row.target = this._resolve(row.entry);
|
|
905
|
+
for (const row of this._rows) {
|
|
906
|
+
if (row.target && !this._byTarget.has(row.target)) this._byTarget.set(row.target, row);
|
|
907
|
+
}
|
|
908
|
+
const container = this.config.scrollContainer;
|
|
909
|
+
const IO = this.config.IntersectionObserver || globalThis.IntersectionObserver;
|
|
910
|
+
if (!container || typeof IO !== "function") return;
|
|
911
|
+
if (!this._observer) {
|
|
912
|
+
this._observer = new IO((records) => this._intersect(records), {
|
|
913
|
+
root: container,
|
|
914
|
+
rootMargin: this.config.rootMargin,
|
|
915
|
+
threshold: 0
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
for (const target of this._byTarget.keys()) this._observer.observe(target);
|
|
919
|
+
}
|
|
920
|
+
_intersect(records) {
|
|
921
|
+
if (this._disposed) return;
|
|
922
|
+
for (const rec of records) {
|
|
923
|
+
const row = this._byTarget.get(rec.target);
|
|
924
|
+
if (!row) continue;
|
|
925
|
+
if (rec.isIntersecting) this._inView.add(row);
|
|
926
|
+
else this._inView.delete(row);
|
|
927
|
+
}
|
|
928
|
+
if (this._suppressed) return;
|
|
929
|
+
const top = this._rows.find((row) => this._inView.has(row));
|
|
930
|
+
if (top) this._activate(top.id);
|
|
931
|
+
}
|
|
932
|
+
_click(ev) {
|
|
933
|
+
const li = ev.target?.closest?.(".twm-doc-outline__item");
|
|
934
|
+
if (!li || !this.listElement.contains(li)) return;
|
|
935
|
+
const row = this._rows.find((r) => r.li === li);
|
|
936
|
+
if (!row) return;
|
|
937
|
+
if (row.target) {
|
|
938
|
+
this._activate(row.id);
|
|
939
|
+
this._suppress();
|
|
940
|
+
if (this.config.scrollOnSelect && typeof row.target.scrollIntoView === "function") {
|
|
941
|
+
row.target.scrollIntoView({ block: "start", behavior: this.config.scrollBehavior });
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
this.config.onSelect?.(row.id, row.entry);
|
|
945
|
+
}
|
|
946
|
+
_suppress() {
|
|
947
|
+
this._suppressed = true;
|
|
948
|
+
this._clearTimer();
|
|
949
|
+
this._timer = setTimeout(() => this._resume(), this.config.scrollEndTimeout);
|
|
950
|
+
}
|
|
951
|
+
_resume() {
|
|
952
|
+
this._clearTimer();
|
|
953
|
+
this._suppressed = false;
|
|
954
|
+
}
|
|
955
|
+
_clearTimer() {
|
|
956
|
+
if (this._timer !== null) clearTimeout(this._timer);
|
|
957
|
+
this._timer = null;
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
function createDocumentOutline(host, config = {}) {
|
|
961
|
+
return new DocumentOutline(host, config);
|
|
962
|
+
}
|
|
963
|
+
function numberEntries(entries, maxDepth) {
|
|
964
|
+
const cap = Math.max(1, Math.floor(Number(maxDepth) || 3));
|
|
965
|
+
const open = [];
|
|
966
|
+
const counters = [];
|
|
967
|
+
return entries.map((entry) => {
|
|
968
|
+
const level = Math.max(1, Math.floor(Number(entry?.level) || 1));
|
|
969
|
+
while (open.length && open[open.length - 1] >= level) open.pop();
|
|
970
|
+
const depth = Math.min(cap, open.length + 1);
|
|
971
|
+
open.push(level);
|
|
972
|
+
counters.length = depth;
|
|
973
|
+
counters[depth - 1] = (counters[depth - 1] || 0) + 1;
|
|
974
|
+
return { id: String(entry?.id), entry, depth, number: counters.join("."), li: null, target: null };
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/help/help_modal.js
|
|
979
|
+
var TOC_HEADINGS = "h1, h2, h3, h4, h5, h6";
|
|
980
|
+
var TOC_MIN_HEADINGS = 2;
|
|
981
|
+
var _activeModal = null;
|
|
982
|
+
var HelpModal = class _HelpModal {
|
|
983
|
+
/** The application's help, resolved at use time. */
|
|
984
|
+
get _service() {
|
|
985
|
+
return helpProvider();
|
|
986
|
+
}
|
|
987
|
+
get _categories() {
|
|
988
|
+
return helpCategories();
|
|
989
|
+
}
|
|
990
|
+
get _copy() {
|
|
991
|
+
return helpCopy();
|
|
992
|
+
}
|
|
993
|
+
constructor() {
|
|
994
|
+
this._overlay = null;
|
|
995
|
+
this._modal = null;
|
|
996
|
+
this._currentTopic = null;
|
|
997
|
+
this._searchQuery = "";
|
|
998
|
+
this._fallback = null;
|
|
999
|
+
this._toc = null;
|
|
1000
|
+
this._boundKeyHandler = this._handleKeyDown.bind(this);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Open the help modal.
|
|
1004
|
+
* @param {string} [topicId] - Initial topic to display
|
|
1005
|
+
* @param {Object} [options]
|
|
1006
|
+
* @param {{title: string, html: string}} [options.fallback] - Shown for
|
|
1007
|
+
* `topicId` when the provider has no such topic (its `loadTopic` answers
|
|
1008
|
+
* null). A library-owned topic, such as the palette's keyboard shortcuts,
|
|
1009
|
+
* uses it so that an application's own topic of that id still wins.
|
|
1010
|
+
*/
|
|
1011
|
+
async open(topicId = null, { fallback = null } = {}) {
|
|
1012
|
+
this._fallback = topicId && fallback ? { id: topicId, topic: fallback } : null;
|
|
1013
|
+
if (_activeModal && _activeModal !== this) {
|
|
1014
|
+
_activeModal.close();
|
|
1015
|
+
}
|
|
1016
|
+
_activeModal = this;
|
|
1017
|
+
this._createModal();
|
|
1018
|
+
document.body.appendChild(this._overlay);
|
|
1019
|
+
document.addEventListener("keydown", this._boundKeyHandler);
|
|
1020
|
+
const searchInput = this._modal.querySelector(".help-search-input");
|
|
1021
|
+
if (searchInput) {
|
|
1022
|
+
setTimeout(() => searchInput.focus(), 100);
|
|
1023
|
+
}
|
|
1024
|
+
if (topicId) {
|
|
1025
|
+
await this._loadTopic(topicId);
|
|
1026
|
+
} else {
|
|
1027
|
+
this._showIndex();
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Close the help modal.
|
|
1032
|
+
*/
|
|
1033
|
+
close() {
|
|
1034
|
+
this._disposeToc();
|
|
1035
|
+
if (this._overlay) {
|
|
1036
|
+
document.removeEventListener("keydown", this._boundKeyHandler);
|
|
1037
|
+
this._overlay.remove();
|
|
1038
|
+
this._overlay = null;
|
|
1039
|
+
this._modal = null;
|
|
1040
|
+
}
|
|
1041
|
+
if (_activeModal === this) {
|
|
1042
|
+
_activeModal = null;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Create the modal DOM structure.
|
|
1047
|
+
* @private
|
|
1048
|
+
*/
|
|
1049
|
+
_createModal() {
|
|
1050
|
+
this._overlay = document.createElement("div");
|
|
1051
|
+
this._overlay.className = "help-modal-overlay";
|
|
1052
|
+
this._overlay.addEventListener("click", (e) => {
|
|
1053
|
+
if (e.target === this._overlay) this.close();
|
|
1054
|
+
});
|
|
1055
|
+
this._modal = document.createElement("div");
|
|
1056
|
+
this._modal.className = "help-modal";
|
|
1057
|
+
this._modal.innerHTML = `
|
|
1058
|
+
<div class="help-modal__header">
|
|
1059
|
+
<div class="help-modal__title">
|
|
1060
|
+
<span class="material-symbols-outlined">help</span>
|
|
1061
|
+
<span>Help</span>
|
|
1062
|
+
</div>
|
|
1063
|
+
<button class="help-modal__close" title="Close (Esc)">
|
|
1064
|
+
<span class="material-symbols-outlined">close</span>
|
|
1065
|
+
</button>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div class="help-modal__search">
|
|
1068
|
+
<span class="material-symbols-outlined">search</span>
|
|
1069
|
+
<input type="text" class="help-search-input" placeholder="Search help topics..." autocomplete="off">
|
|
1070
|
+
</div>
|
|
1071
|
+
<div class="help-modal__body">
|
|
1072
|
+
<nav class="help-modal__sidebar">
|
|
1073
|
+
<div class="help-sidebar__categories"></div>
|
|
1074
|
+
</nav>
|
|
1075
|
+
<main class="help-modal__content">
|
|
1076
|
+
<div class="help-content__loading">Loading...</div>
|
|
1077
|
+
</main>
|
|
1078
|
+
</div>
|
|
1079
|
+
`;
|
|
1080
|
+
const closeBtn = this._modal.querySelector(".help-modal__close");
|
|
1081
|
+
closeBtn.addEventListener("click", () => this.close());
|
|
1082
|
+
const searchInput = this._modal.querySelector(".help-search-input");
|
|
1083
|
+
searchInput.addEventListener("input", (e) => this._handleSearch(e.target.value));
|
|
1084
|
+
searchInput.addEventListener("keydown", (e) => {
|
|
1085
|
+
if (e.key === "Escape") {
|
|
1086
|
+
if (e.target.value) {
|
|
1087
|
+
e.target.value = "";
|
|
1088
|
+
this._handleSearch("");
|
|
1089
|
+
e.stopPropagation();
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
this._buildSidebar();
|
|
1094
|
+
this._overlay.appendChild(this._modal);
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Build the sidebar navigation.
|
|
1098
|
+
* @private
|
|
1099
|
+
*/
|
|
1100
|
+
_buildSidebar() {
|
|
1101
|
+
const container = this._modal.querySelector(".help-sidebar__categories");
|
|
1102
|
+
const categories = this._service.getCategories();
|
|
1103
|
+
const categoryOrder = Object.keys(this._categories);
|
|
1104
|
+
categories.sort((a, b) => {
|
|
1105
|
+
const orderA = categoryOrder.indexOf(a);
|
|
1106
|
+
const orderB = categoryOrder.indexOf(b);
|
|
1107
|
+
return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);
|
|
1108
|
+
});
|
|
1109
|
+
container.innerHTML = categories.map((category) => {
|
|
1110
|
+
const topics = this._service.getTopicsByCategory(category);
|
|
1111
|
+
const label = this._categories[category]?.label || category;
|
|
1112
|
+
const icon = this._categories[category]?.icon || "folder";
|
|
1113
|
+
return `
|
|
1114
|
+
<div class="help-category" data-category="${escapeHtml2(category)}">
|
|
1115
|
+
<div class="help-category__header">
|
|
1116
|
+
<span class="material-symbols-outlined">${escapeHtml2(icon)}</span>
|
|
1117
|
+
<span>${escapeHtml2(label)}</span>
|
|
1118
|
+
</div>
|
|
1119
|
+
<ul class="help-category__topics">
|
|
1120
|
+
${topics.map((topic) => `
|
|
1121
|
+
<li class="help-topic-item" data-topic="${escapeHtml2(topic.id)}">
|
|
1122
|
+
${escapeHtml2(topic.title)}
|
|
1123
|
+
</li>
|
|
1124
|
+
`).join("")}
|
|
1125
|
+
</ul>
|
|
1126
|
+
</div>
|
|
1127
|
+
`;
|
|
1128
|
+
}).join("");
|
|
1129
|
+
container.querySelectorAll(".help-topic-item").forEach((item) => {
|
|
1130
|
+
item.addEventListener("click", () => {
|
|
1131
|
+
this._loadTopic(item.dataset.topic);
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
container.querySelectorAll(".help-category__header").forEach((header) => {
|
|
1135
|
+
header.addEventListener("click", () => {
|
|
1136
|
+
header.parentElement.classList.toggle("collapsed");
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Show the help index page.
|
|
1142
|
+
* @private
|
|
1143
|
+
*/
|
|
1144
|
+
_showIndex() {
|
|
1145
|
+
this._disposeToc();
|
|
1146
|
+
const content = this._modal.querySelector(".help-modal__content");
|
|
1147
|
+
const categories = this._service.getCategories();
|
|
1148
|
+
const categoryOrder = Object.keys(this._categories);
|
|
1149
|
+
categories.sort((a, b) => {
|
|
1150
|
+
const orderA = categoryOrder.indexOf(a);
|
|
1151
|
+
const orderB = categoryOrder.indexOf(b);
|
|
1152
|
+
return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);
|
|
1153
|
+
});
|
|
1154
|
+
content.innerHTML = `
|
|
1155
|
+
<div class="help-index">
|
|
1156
|
+
<h1>${this._copy.title}</h1>
|
|
1157
|
+
<p>${this._copy.welcome}</p>
|
|
1158
|
+
|
|
1159
|
+
<div class="help-index__grid">
|
|
1160
|
+
${categories.map((category) => {
|
|
1161
|
+
const topics = this._service.getTopicsByCategory(category);
|
|
1162
|
+
const label = this._categories[category]?.label || category;
|
|
1163
|
+
const icon = this._categories[category]?.icon || "folder";
|
|
1164
|
+
return `
|
|
1165
|
+
<div class="help-index__category" data-category="${escapeHtml2(category)}">
|
|
1166
|
+
<div class="help-index__category-header">
|
|
1167
|
+
<span class="material-symbols-outlined">${escapeHtml2(icon)}</span>
|
|
1168
|
+
<span>${escapeHtml2(label)}</span>
|
|
1169
|
+
</div>
|
|
1170
|
+
<ul class="help-index__topics">
|
|
1171
|
+
${topics.slice(0, 3).map((topic) => `
|
|
1172
|
+
<li class="help-index__topic" data-topic="${escapeHtml2(topic.id)}">
|
|
1173
|
+
${escapeHtml2(topic.title)}
|
|
1174
|
+
</li>
|
|
1175
|
+
`).join("")}
|
|
1176
|
+
${topics.length > 3 ? `<li class="help-index__more">+${topics.length - 3} more</li>` : ""}
|
|
1177
|
+
</ul>
|
|
1178
|
+
</div>
|
|
1179
|
+
`;
|
|
1180
|
+
}).join("")}
|
|
1181
|
+
</div>
|
|
1182
|
+
</div>
|
|
1183
|
+
`;
|
|
1184
|
+
content.querySelectorAll(".help-index__topic").forEach((item) => {
|
|
1185
|
+
item.addEventListener("click", () => {
|
|
1186
|
+
this._loadTopic(item.dataset.topic);
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Load and display a topic.
|
|
1192
|
+
* @param {string} topicId - Topic ID
|
|
1193
|
+
* @private
|
|
1194
|
+
*/
|
|
1195
|
+
async _loadTopic(topicId) {
|
|
1196
|
+
this._disposeToc();
|
|
1197
|
+
const content = this._modal.querySelector(".help-modal__content");
|
|
1198
|
+
content.innerHTML = '<div class="help-content__loading">Loading...</div>';
|
|
1199
|
+
this._modal.querySelectorAll(".help-topic-item").forEach((item) => {
|
|
1200
|
+
item.classList.toggle("active", item.dataset.topic === topicId);
|
|
1201
|
+
});
|
|
1202
|
+
try {
|
|
1203
|
+
const topic = await this._service.loadTopic(topicId) ?? (this._fallback?.id === topicId ? this._fallback.topic : null);
|
|
1204
|
+
if (!this._modal?.contains(content)) return;
|
|
1205
|
+
this._currentTopic = topicId;
|
|
1206
|
+
if (!topic) {
|
|
1207
|
+
content.innerHTML = `
|
|
1208
|
+
<div class="help-content__error help-content__error--not-found">
|
|
1209
|
+
<span class="material-symbols-outlined">search_off</span>
|
|
1210
|
+
<p>Help topic not found.</p>
|
|
1211
|
+
</div>
|
|
1212
|
+
`;
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
content.innerHTML = `
|
|
1216
|
+
<div class="help-content__article">
|
|
1217
|
+
<div class="help-content__breadcrumb">
|
|
1218
|
+
<a href="#" class="help-breadcrumb__home" title="Help Index">
|
|
1219
|
+
<span class="material-symbols-outlined">home</span>
|
|
1220
|
+
</a>
|
|
1221
|
+
<span class="help-breadcrumb__separator">/</span>
|
|
1222
|
+
<span class="help-breadcrumb__title">${escapeHtml2(topic.title)}</span>
|
|
1223
|
+
</div>
|
|
1224
|
+
<div class="help-content__toc" hidden></div>
|
|
1225
|
+
<article class="help-article">
|
|
1226
|
+
${this._articleHtml(topic)}
|
|
1227
|
+
</article>
|
|
1228
|
+
</div>
|
|
1229
|
+
`;
|
|
1230
|
+
content.querySelector(".help-breadcrumb__home").addEventListener("click", (e) => {
|
|
1231
|
+
e.preventDefault();
|
|
1232
|
+
this._showIndex();
|
|
1233
|
+
});
|
|
1234
|
+
this._buildToc(content);
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
content.innerHTML = `
|
|
1237
|
+
<div class="help-content__error">
|
|
1238
|
+
<span class="material-symbols-outlined">error</span>
|
|
1239
|
+
<p>Failed to load help topic.</p>
|
|
1240
|
+
</div>
|
|
1241
|
+
`;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* The article body: a markdown topic rendered safe, else the provider's
|
|
1246
|
+
* own HTML, which is trusted by contract.
|
|
1247
|
+
* @private
|
|
1248
|
+
*/
|
|
1249
|
+
_articleHtml(topic) {
|
|
1250
|
+
if (typeof topic.markdown === "string") {
|
|
1251
|
+
return renderMarkdown(topic.markdown, this._service.markdownOptions ?? {});
|
|
1252
|
+
}
|
|
1253
|
+
return topic.html ?? "";
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* The topic's table of contents: one entry per heading in the article, in
|
|
1257
|
+
* document order, its level the heading's. Built only when the article
|
|
1258
|
+
* has `TOC_MIN_HEADINGS` or more; the outline resolves each entry to its
|
|
1259
|
+
* heading ELEMENT, so the provider's markup needs no ids and the modal
|
|
1260
|
+
* adds none.
|
|
1261
|
+
* @param {HTMLElement} content - The scrolling content pane.
|
|
1262
|
+
* @private
|
|
1263
|
+
*/
|
|
1264
|
+
_buildToc(content) {
|
|
1265
|
+
this._disposeToc();
|
|
1266
|
+
const host = content.querySelector(".help-content__toc");
|
|
1267
|
+
const headings = [...content.querySelectorAll(`.help-article :is(${TOC_HEADINGS})`)];
|
|
1268
|
+
if (!host || headings.length < TOC_MIN_HEADINGS) return;
|
|
1269
|
+
const byId = new Map(headings.map((heading, index) => [`heading-${index}`, heading]));
|
|
1270
|
+
const entries = [...byId].map(([id, heading]) => ({
|
|
1271
|
+
id,
|
|
1272
|
+
level: Number(heading.tagName.slice(1)),
|
|
1273
|
+
label: heading.textContent.trim()
|
|
1274
|
+
}));
|
|
1275
|
+
host.hidden = false;
|
|
1276
|
+
this._toc = createDocumentOutline(host, {
|
|
1277
|
+
entries,
|
|
1278
|
+
numbering: true,
|
|
1279
|
+
scrollContainer: content,
|
|
1280
|
+
resolveTarget: (entry) => byId.get(entry.id) ?? null,
|
|
1281
|
+
label: "Contents"
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
/** @private */
|
|
1285
|
+
_disposeToc() {
|
|
1286
|
+
this._toc?.dispose();
|
|
1287
|
+
this._toc = null;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Handle search input.
|
|
1291
|
+
* @param {string} query - Search query
|
|
1292
|
+
* @private
|
|
1293
|
+
*/
|
|
1294
|
+
_handleSearch(query) {
|
|
1295
|
+
this._searchQuery = query;
|
|
1296
|
+
const content = this._modal.querySelector(".help-modal__content");
|
|
1297
|
+
if (!query.trim()) {
|
|
1298
|
+
this._showIndex();
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
this._disposeToc();
|
|
1302
|
+
const results = this._service.searchTopics(query);
|
|
1303
|
+
if (results.length === 0) {
|
|
1304
|
+
content.innerHTML = `
|
|
1305
|
+
<div class="help-search-results">
|
|
1306
|
+
<h2>Search Results</h2>
|
|
1307
|
+
<p class="help-search-empty">No results found for "${escapeHtml2(query)}"</p>
|
|
1308
|
+
</div>
|
|
1309
|
+
`;
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
content.innerHTML = `
|
|
1313
|
+
<div class="help-search-results">
|
|
1314
|
+
<h2>Search Results</h2>
|
|
1315
|
+
<p class="help-search-count">${results.length} result${results.length !== 1 ? "s" : ""} for "${escapeHtml2(query)}"</p>
|
|
1316
|
+
<ul class="help-search-list">
|
|
1317
|
+
${results.map((topic) => `
|
|
1318
|
+
<li class="help-search-item" data-topic="${escapeHtml2(topic.id)}">
|
|
1319
|
+
<span class="help-search-item__title">${escapeHtml2(topic.title)}</span>
|
|
1320
|
+
<span class="help-search-item__category">${escapeHtml2(this._categories[topic.category]?.label || topic.category)}</span>
|
|
1321
|
+
</li>
|
|
1322
|
+
`).join("")}
|
|
1323
|
+
</ul>
|
|
1324
|
+
</div>
|
|
1325
|
+
`;
|
|
1326
|
+
content.querySelectorAll(".help-search-item").forEach((item) => {
|
|
1327
|
+
item.addEventListener("click", () => {
|
|
1328
|
+
this._loadTopic(item.dataset.topic);
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Handle keyboard events.
|
|
1334
|
+
* @param {KeyboardEvent} event
|
|
1335
|
+
* @private
|
|
1336
|
+
*/
|
|
1337
|
+
_handleKeyDown(event) {
|
|
1338
|
+
if (event.key === "Escape") {
|
|
1339
|
+
this.close();
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Static method to open help modal.
|
|
1344
|
+
* @param {string} [topicId] - Initial topic
|
|
1345
|
+
* @param {Object} [options] - As for the instance `open`.
|
|
1346
|
+
* @returns {HelpModal}
|
|
1347
|
+
*/
|
|
1348
|
+
static open(topicId = null, options = {}) {
|
|
1349
|
+
const modal = new _HelpModal();
|
|
1350
|
+
modal.open(topicId, options);
|
|
1351
|
+
return modal;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Close the open help modal, or open the index when none is open — what
|
|
1355
|
+
* the `?` shortcut does.
|
|
1356
|
+
* @returns {HelpModal|null} the modal opened, or null when one was closed
|
|
1357
|
+
*/
|
|
1358
|
+
static toggle() {
|
|
1359
|
+
if (_activeModal) {
|
|
1360
|
+
_activeModal.close();
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
return _HelpModal.open();
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Get the currently active modal.
|
|
1367
|
+
* @returns {HelpModal|null}
|
|
1368
|
+
*/
|
|
1369
|
+
static getActive() {
|
|
1370
|
+
return _activeModal;
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
function escapeHtml2(value) {
|
|
1374
|
+
if (value === null || value === void 0) return "";
|
|
1375
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1376
|
+
}
|
|
1377
|
+
function installHelpShortcut({ target = document } = {}) {
|
|
1378
|
+
const onKeyDown = (event) => {
|
|
1379
|
+
if (event.defaultPrevented || event.key !== "?") return;
|
|
1380
|
+
if (event.target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
|
|
1381
|
+
event.preventDefault();
|
|
1382
|
+
HelpModal.toggle();
|
|
1383
|
+
};
|
|
1384
|
+
target.addEventListener("keydown", onKeyDown);
|
|
1385
|
+
return () => target.removeEventListener("keydown", onKeyDown);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// src/ui/components/menu_bar.js
|
|
1389
|
+
var SEL_TITLE = ".twm-menubar__title";
|
|
1390
|
+
var SEL_ITEM = ".twm-menubar__item";
|
|
1391
|
+
var SEL_MENU = ".twm-menubar__menu";
|
|
1392
|
+
function createMenuBar(el, opts) {
|
|
1393
|
+
return new MenuBar(el, opts);
|
|
1394
|
+
}
|
|
1395
|
+
var MenuBar = class {
|
|
1396
|
+
/**
|
|
1397
|
+
* @param {Element} el the element that becomes the bar
|
|
1398
|
+
* @param {object} opts
|
|
1399
|
+
* @param {Array} opts.menus `[{ id, label, items }]`
|
|
1400
|
+
* @param {Function} [opts.onAction] `(id, { item, checked }) => void`
|
|
1401
|
+
* @param {string} [opts.label] the bar's accessible name ('Menu')
|
|
1402
|
+
* @param {string} [opts.emptyLabel] the disabled row an empty submenu shows ('Empty')
|
|
1403
|
+
*/
|
|
1404
|
+
constructor(el, { menus, onAction = null, label = "Menu", emptyLabel = "Empty" } = {}) {
|
|
1405
|
+
if (!el || typeof el.appendChild !== "function") {
|
|
1406
|
+
throw new TypeError("createMenuBar: `el` must be an element");
|
|
1407
|
+
}
|
|
1408
|
+
_validateMenus(menus);
|
|
1409
|
+
this.el = el;
|
|
1410
|
+
this.menus = menus;
|
|
1411
|
+
this._onAction = typeof onAction === "function" ? onAction : null;
|
|
1412
|
+
this._emptyLabel = emptyLabel;
|
|
1413
|
+
this._doc = el.ownerDocument;
|
|
1414
|
+
this._disabled = /* @__PURE__ */ new Map();
|
|
1415
|
+
this._checked = /* @__PURE__ */ new Map();
|
|
1416
|
+
this._radio = /* @__PURE__ */ new Map();
|
|
1417
|
+
this._models = /* @__PURE__ */ new WeakMap();
|
|
1418
|
+
this._stack = [];
|
|
1419
|
+
this._openIdx = -1;
|
|
1420
|
+
this._active = false;
|
|
1421
|
+
this._returnTo = null;
|
|
1422
|
+
this._altArmed = false;
|
|
1423
|
+
el.innerHTML = "";
|
|
1424
|
+
el.classList.add("twm-menubar");
|
|
1425
|
+
el.setAttribute("role", "menubar");
|
|
1426
|
+
el.setAttribute("aria-label", label);
|
|
1427
|
+
this._titles = menus.map((m, i) => {
|
|
1428
|
+
const entry = this._doc.createElement("div");
|
|
1429
|
+
entry.className = "twm-menubar__entry";
|
|
1430
|
+
entry.setAttribute("role", "none");
|
|
1431
|
+
const btn = this._doc.createElement("button");
|
|
1432
|
+
btn.type = "button";
|
|
1433
|
+
btn.className = "twm-menubar__title";
|
|
1434
|
+
btn.setAttribute("role", "menuitem");
|
|
1435
|
+
btn.setAttribute("aria-haspopup", "menu");
|
|
1436
|
+
btn.setAttribute("aria-expanded", "false");
|
|
1437
|
+
btn.tabIndex = i === 0 ? 0 : -1;
|
|
1438
|
+
btn.textContent = m.label;
|
|
1439
|
+
entry.appendChild(btn);
|
|
1440
|
+
el.appendChild(entry);
|
|
1441
|
+
return btn;
|
|
1442
|
+
});
|
|
1443
|
+
this._h = {
|
|
1444
|
+
mousedown: (e) => this._onMouseDown(e),
|
|
1445
|
+
click: (e) => this._onClick(e),
|
|
1446
|
+
mouseover: (e) => this._onMouseOver(e),
|
|
1447
|
+
keydown: (e) => this._onKey(e),
|
|
1448
|
+
keyup: (e) => {
|
|
1449
|
+
if (e.key === " " && this.el.contains(e.target)) e.preventDefault();
|
|
1450
|
+
},
|
|
1451
|
+
focusin: (e) => this._onFocusIn(e),
|
|
1452
|
+
focusout: (e) => this._onFocusOut(e),
|
|
1453
|
+
docKeydown: (e) => this._onDocKeyDown(e),
|
|
1454
|
+
docKeyup: (e) => this._onDocKeyUp(e),
|
|
1455
|
+
docMousedown: (e) => this._onDocMouseDown(e),
|
|
1456
|
+
// Alt+Tab, Alt+F4, Alt+Space: the second key goes to the OS, so no
|
|
1457
|
+
// keydown reaches the page to disarm Alt. The window losing focus
|
|
1458
|
+
// does, or the Alt keyup on return would take focus to the bar.
|
|
1459
|
+
winBlur: () => {
|
|
1460
|
+
this._altArmed = false;
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
for (const k of ["mousedown", "click", "mouseover", "keydown", "keyup", "focusin", "focusout"]) {
|
|
1464
|
+
el.addEventListener(k, this._h[k]);
|
|
1465
|
+
}
|
|
1466
|
+
this._doc.addEventListener("keydown", this._h.docKeydown, true);
|
|
1467
|
+
this._doc.addEventListener("keyup", this._h.docKeyup, true);
|
|
1468
|
+
this._doc.addEventListener("mousedown", this._h.docMousedown, true);
|
|
1469
|
+
this._doc.defaultView?.addEventListener("blur", this._h.winBlur);
|
|
1470
|
+
}
|
|
1471
|
+
// ── public ────────────────────────────────────────────────────────
|
|
1472
|
+
/** Whether a menu is open. */
|
|
1473
|
+
isOpen() {
|
|
1474
|
+
return this._openIdx >= 0;
|
|
1475
|
+
}
|
|
1476
|
+
/** Open the menu with this id (focus on its title, or on its first item). */
|
|
1477
|
+
open(menuId, { focusItem = false } = {}) {
|
|
1478
|
+
const i = this.menus.findIndex((m) => m.id === menuId);
|
|
1479
|
+
if (i < 0) return;
|
|
1480
|
+
this._enter();
|
|
1481
|
+
this._open(i, focusItem ? "first" : null);
|
|
1482
|
+
}
|
|
1483
|
+
/** Close every open menu. Focus stays in the bar if it was there. */
|
|
1484
|
+
close() {
|
|
1485
|
+
this._closeAll();
|
|
1486
|
+
}
|
|
1487
|
+
/** Focus the bar's first title, as Alt or F10 does. */
|
|
1488
|
+
focus() {
|
|
1489
|
+
this._enter();
|
|
1490
|
+
this._focusTitle(0);
|
|
1491
|
+
}
|
|
1492
|
+
isEnabled(id) {
|
|
1493
|
+
const item = this._findModel(id);
|
|
1494
|
+
return item ? !this._isDisabled(item) : !this._disabled.get(id);
|
|
1495
|
+
}
|
|
1496
|
+
isChecked(id) {
|
|
1497
|
+
const item = this._findModel(id);
|
|
1498
|
+
return item ? this._isChecked(item) : !!this._checked.get(id);
|
|
1499
|
+
}
|
|
1500
|
+
/** Enable or disable every item with this id; an open menu is patched in place. */
|
|
1501
|
+
setEnabled(id, enabled) {
|
|
1502
|
+
this._disabled.set(id, !enabled);
|
|
1503
|
+
this._patch(id);
|
|
1504
|
+
}
|
|
1505
|
+
/** Check or uncheck every item with this id; checking a radio item
|
|
1506
|
+
* unchecks the rest of its group. An open menu is patched in place. */
|
|
1507
|
+
setChecked(id, checked) {
|
|
1508
|
+
this._check(this._findModel(id), id, checked);
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* The model as command-palette rows: one per enabled leaf item, labelled
|
|
1512
|
+
* as in the menu, with its `shortcut`, and its menu path as `context`
|
|
1513
|
+
* ('File › Open recent'). A function submenu is called now, so the rows
|
|
1514
|
+
* are as current as the menu would be. `action` runs the item exactly as
|
|
1515
|
+
* a click would, including flipping a check or radio item.
|
|
1516
|
+
*/
|
|
1517
|
+
commands() {
|
|
1518
|
+
const rows = [];
|
|
1519
|
+
const walk = (items, path) => {
|
|
1520
|
+
for (const it of _resolve(items)) {
|
|
1521
|
+
if (!it || it.separator || this._isDisabled(it)) continue;
|
|
1522
|
+
if (it.submenu != null) {
|
|
1523
|
+
walk(it.submenu, [...path, it.label]);
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
rows.push({
|
|
1527
|
+
id: `menubar:${it.id}`,
|
|
1528
|
+
label: it.label,
|
|
1529
|
+
...it.shortcut ? { shortcut: it.shortcut } : {},
|
|
1530
|
+
context: path.join(" \u203A "),
|
|
1531
|
+
verb: "run",
|
|
1532
|
+
action: () => this._fire(it)
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
for (const m of this.menus) walk(m.items, [m.label]);
|
|
1537
|
+
return rows;
|
|
1538
|
+
}
|
|
1539
|
+
dispose() {
|
|
1540
|
+
this._closeAll();
|
|
1541
|
+
for (const k of ["mousedown", "click", "mouseover", "keydown", "keyup", "focusin", "focusout"]) {
|
|
1542
|
+
this.el.removeEventListener(k, this._h[k]);
|
|
1543
|
+
}
|
|
1544
|
+
this._doc.removeEventListener("keydown", this._h.docKeydown, true);
|
|
1545
|
+
this._doc.removeEventListener("keyup", this._h.docKeyup, true);
|
|
1546
|
+
this._doc.removeEventListener("mousedown", this._h.docMousedown, true);
|
|
1547
|
+
this._doc.defaultView?.removeEventListener("blur", this._h.winBlur);
|
|
1548
|
+
this.el.innerHTML = "";
|
|
1549
|
+
this.el.classList.remove("twm-menubar");
|
|
1550
|
+
this.el.removeAttribute("role");
|
|
1551
|
+
this.el.removeAttribute("aria-label");
|
|
1552
|
+
}
|
|
1553
|
+
// ── state ─────────────────────────────────────────────────────────
|
|
1554
|
+
_isDisabled(item) {
|
|
1555
|
+
return this._disabled.has(item.id) ? this._disabled.get(item.id) : !!item.disabled;
|
|
1556
|
+
}
|
|
1557
|
+
_isChecked(item) {
|
|
1558
|
+
if (item.radio != null && this._radio.has(item.radio)) return this._radio.get(item.radio) === item.id;
|
|
1559
|
+
return this._checked.has(item.id) ? this._checked.get(item.id) : !!item.checked;
|
|
1560
|
+
}
|
|
1561
|
+
/** Record a check. `item` is the model item when the bar knows it (a
|
|
1562
|
+
* radio item then records its group's selection); `id` alone is kept as
|
|
1563
|
+
* an override for an item the model does not hold yet. */
|
|
1564
|
+
_check(item, id, checked) {
|
|
1565
|
+
if (item?.radio != null) {
|
|
1566
|
+
const group = item.radio;
|
|
1567
|
+
if (checked) this._radio.set(group, id);
|
|
1568
|
+
else if (this._isChecked(item)) this._radio.set(group, null);
|
|
1569
|
+
this._patchGroup(group);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
this._checked.set(id, !!checked);
|
|
1573
|
+
this._patch(id);
|
|
1574
|
+
}
|
|
1575
|
+
/** Every model item the bar knows of: the model, with every function
|
|
1576
|
+
* submenu called now (as `commands()` does), and whatever is on screen. */
|
|
1577
|
+
_knownItems() {
|
|
1578
|
+
const out = [];
|
|
1579
|
+
const walk = (items) => {
|
|
1580
|
+
for (const it of _resolve(items)) {
|
|
1581
|
+
if (!it || it.separator) continue;
|
|
1582
|
+
out.push(it);
|
|
1583
|
+
if (it.submenu != null) walk(it.submenu);
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
for (const m of this.menus) walk(m.items);
|
|
1587
|
+
for (const b of this.el.querySelectorAll(SEL_ITEM)) {
|
|
1588
|
+
const it = this._models.get(b);
|
|
1589
|
+
if (it && !out.includes(it)) out.push(it);
|
|
1590
|
+
}
|
|
1591
|
+
return out;
|
|
1592
|
+
}
|
|
1593
|
+
_findModel(id) {
|
|
1594
|
+
return this._knownItems().find((it) => it.id === id) || null;
|
|
1595
|
+
}
|
|
1596
|
+
/** Re-apply the state of every rendered item with this id. */
|
|
1597
|
+
_patch(id) {
|
|
1598
|
+
for (const b of this.el.querySelectorAll(SEL_ITEM)) {
|
|
1599
|
+
const it = this._models.get(b);
|
|
1600
|
+
if (it && it.id === id) this._apply(b, it);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/** Re-apply the state of every rendered item of this radio group. */
|
|
1604
|
+
_patchGroup(group) {
|
|
1605
|
+
for (const b of this.el.querySelectorAll(SEL_ITEM)) {
|
|
1606
|
+
const it = this._models.get(b);
|
|
1607
|
+
if (it && it.radio === group) this._apply(b, it);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
_apply(btn, item) {
|
|
1611
|
+
const off = this._isDisabled(item);
|
|
1612
|
+
btn.classList.toggle("twm-menubar__item--disabled", off);
|
|
1613
|
+
if (off) btn.setAttribute("aria-disabled", "true");
|
|
1614
|
+
else btn.removeAttribute("aria-disabled");
|
|
1615
|
+
if (_kind(item) !== "menuitem") {
|
|
1616
|
+
btn.setAttribute("aria-checked", this._isChecked(item) ? "true" : "false");
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
// ── open / close ──────────────────────────────────────────────────
|
|
1620
|
+
_setRoving(i) {
|
|
1621
|
+
this._titles.forEach((t, j) => {
|
|
1622
|
+
t.tabIndex = j === i ? 0 : -1;
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
_focusTitle(i) {
|
|
1626
|
+
this._setRoving(i);
|
|
1627
|
+
this._titles[i]?.focus({ preventScroll: true });
|
|
1628
|
+
}
|
|
1629
|
+
/** The bar becomes active; remember what to give focus back to. */
|
|
1630
|
+
_enter(from = this._doc.activeElement) {
|
|
1631
|
+
if (this._active) return;
|
|
1632
|
+
this._active = true;
|
|
1633
|
+
this._returnTo = from && from !== this._doc.body && !this.el.contains(from) ? from : null;
|
|
1634
|
+
}
|
|
1635
|
+
/** Leave the bar and give focus back. */
|
|
1636
|
+
_exit() {
|
|
1637
|
+
const back = this._returnTo;
|
|
1638
|
+
this._closeAll();
|
|
1639
|
+
this._active = false;
|
|
1640
|
+
this._returnTo = null;
|
|
1641
|
+
if (back?.isConnected) back.focus({ preventScroll: true });
|
|
1642
|
+
else if (this.el.contains(this._doc.activeElement)) this._doc.activeElement.blur();
|
|
1643
|
+
}
|
|
1644
|
+
/** Focus went elsewhere on purpose: close, give nothing back. */
|
|
1645
|
+
_deactivate() {
|
|
1646
|
+
this._closeAll();
|
|
1647
|
+
this._active = false;
|
|
1648
|
+
this._returnTo = null;
|
|
1649
|
+
}
|
|
1650
|
+
_open(i, focus) {
|
|
1651
|
+
const n = this._titles.length;
|
|
1652
|
+
i = (i % n + n) % n;
|
|
1653
|
+
this._focusTitle(i);
|
|
1654
|
+
this._closeAll();
|
|
1655
|
+
const m = this.menus[i];
|
|
1656
|
+
const title = this._titles[i];
|
|
1657
|
+
const menu = this._buildMenu(m.items, m.label);
|
|
1658
|
+
title.parentElement.appendChild(menu);
|
|
1659
|
+
title.setAttribute("aria-expanded", "true");
|
|
1660
|
+
this._stack = [{ menu, parent: title }];
|
|
1661
|
+
this._openIdx = i;
|
|
1662
|
+
_place(menu, title, "below");
|
|
1663
|
+
this._focusIn(menu, focus);
|
|
1664
|
+
}
|
|
1665
|
+
_openSub(btn, focus) {
|
|
1666
|
+
const item = this._models.get(btn);
|
|
1667
|
+
if (!item || item.submenu == null || this._isDisabled(item)) return;
|
|
1668
|
+
const level = this._levelOf(btn);
|
|
1669
|
+
if (level < 0) return;
|
|
1670
|
+
const open = this._stack[level + 1];
|
|
1671
|
+
if (open && open.parent === btn) {
|
|
1672
|
+
this._focusIn(open.menu, focus);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
this._closeFrom(level + 1);
|
|
1676
|
+
const menu = this._buildMenu(item.submenu, item.label);
|
|
1677
|
+
btn.parentElement.appendChild(menu);
|
|
1678
|
+
btn.setAttribute("aria-expanded", "true");
|
|
1679
|
+
this._stack.push({ menu, parent: btn });
|
|
1680
|
+
_place(menu, btn, "right");
|
|
1681
|
+
this._focusIn(menu, focus);
|
|
1682
|
+
}
|
|
1683
|
+
_focusIn(menu, focus) {
|
|
1684
|
+
if (!focus) return;
|
|
1685
|
+
const items = this._enabledIn(menu);
|
|
1686
|
+
(focus === "last" ? items[items.length - 1] : items[0])?.focus({ preventScroll: true });
|
|
1687
|
+
}
|
|
1688
|
+
/** Close the menus at `level` and deeper. Focus inside them goes to the
|
|
1689
|
+
* item (or title) they hang from. */
|
|
1690
|
+
_closeFrom(level) {
|
|
1691
|
+
if (level >= this._stack.length) return;
|
|
1692
|
+
const first = this._stack[level];
|
|
1693
|
+
if (this._stack.slice(level).some((s) => s.menu.contains(this._doc.activeElement))) {
|
|
1694
|
+
first.parent.focus({ preventScroll: true });
|
|
1695
|
+
}
|
|
1696
|
+
for (const s of this._stack.splice(level)) {
|
|
1697
|
+
s.menu.remove();
|
|
1698
|
+
s.parent.setAttribute("aria-expanded", "false");
|
|
1699
|
+
}
|
|
1700
|
+
if (level === 0) this._openIdx = -1;
|
|
1701
|
+
}
|
|
1702
|
+
_closeAll() {
|
|
1703
|
+
this._closeFrom(0);
|
|
1704
|
+
}
|
|
1705
|
+
_levelOf(el) {
|
|
1706
|
+
const menu = el.closest(SEL_MENU);
|
|
1707
|
+
return this._stack.findIndex((s) => s.menu === menu);
|
|
1708
|
+
}
|
|
1709
|
+
/** The items the keyboard can land on: no separators, nothing disabled. */
|
|
1710
|
+
_enabledIn(menu) {
|
|
1711
|
+
return this._itemsIn(menu).filter((b) => !this._isDisabled(this._models.get(b)));
|
|
1712
|
+
}
|
|
1713
|
+
/** Every item cell of `menu` in DOM order, disabled ones included. */
|
|
1714
|
+
_itemsIn(menu) {
|
|
1715
|
+
return [...menu.children].map((w) => w.firstElementChild).filter((b) => b?.matches?.(SEL_ITEM));
|
|
1716
|
+
}
|
|
1717
|
+
/** The next (`dir` 1) or previous (-1) enabled item from `btn`'s place
|
|
1718
|
+
* among all the cells, wrapping. `btn` itself may be disabled: one
|
|
1719
|
+
* setEnabled(id, false) leaves focus on it. */
|
|
1720
|
+
_stepFrom(menu, btn, dir) {
|
|
1721
|
+
const all = this._itemsIn(menu);
|
|
1722
|
+
const n = all.length;
|
|
1723
|
+
const at = all.indexOf(btn);
|
|
1724
|
+
for (let k = 1; k <= n; k++) {
|
|
1725
|
+
const b = all[((at + dir * k) % n + n) % n];
|
|
1726
|
+
if (!this._isDisabled(this._models.get(b))) return b;
|
|
1727
|
+
}
|
|
1728
|
+
return null;
|
|
1729
|
+
}
|
|
1730
|
+
_buildMenu(items, label) {
|
|
1731
|
+
const d = this._doc;
|
|
1732
|
+
const menu = d.createElement("div");
|
|
1733
|
+
menu.className = "twm-menubar__menu";
|
|
1734
|
+
menu.setAttribute("role", "menu");
|
|
1735
|
+
menu.setAttribute("aria-label", label);
|
|
1736
|
+
const list = _resolve(items).filter((it) => {
|
|
1737
|
+
if (it && (it.separator || it.id != null && it.label != null)) return true;
|
|
1738
|
+
console.warn("[menubar] an item needs an id and a label", it);
|
|
1739
|
+
return false;
|
|
1740
|
+
});
|
|
1741
|
+
const shown = list.length ? list : [{ id: null, label: this._emptyLabel, disabled: true, _empty: true }];
|
|
1742
|
+
for (const it of shown) {
|
|
1743
|
+
if (it.separator) {
|
|
1744
|
+
const sep = d.createElement("div");
|
|
1745
|
+
sep.className = "twm-menubar__separator";
|
|
1746
|
+
sep.setAttribute("role", "separator");
|
|
1747
|
+
menu.appendChild(sep);
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
const wrap = d.createElement("div");
|
|
1751
|
+
wrap.className = "twm-menubar__cell";
|
|
1752
|
+
wrap.setAttribute("role", "none");
|
|
1753
|
+
const btn = d.createElement("button");
|
|
1754
|
+
btn.type = "button";
|
|
1755
|
+
btn.tabIndex = -1;
|
|
1756
|
+
const kind = _kind(it);
|
|
1757
|
+
btn.className = "twm-menubar__item";
|
|
1758
|
+
if (kind !== "menuitem") btn.classList.add(kind === "menuitemradio" ? "twm-menubar__item--radio" : "twm-menubar__item--check");
|
|
1759
|
+
btn.setAttribute("role", kind);
|
|
1760
|
+
if (it.id != null) btn.dataset.itemId = it.id;
|
|
1761
|
+
const check = d.createElement("span");
|
|
1762
|
+
check.className = "twm-menubar__check";
|
|
1763
|
+
check.setAttribute("aria-hidden", "true");
|
|
1764
|
+
const text = d.createElement("span");
|
|
1765
|
+
text.className = "twm-menubar__label";
|
|
1766
|
+
text.textContent = it.label;
|
|
1767
|
+
btn.append(check, text);
|
|
1768
|
+
if (it.shortcut) {
|
|
1769
|
+
const sc = d.createElement("span");
|
|
1770
|
+
sc.className = "twm-menubar__shortcut";
|
|
1771
|
+
sc.textContent = it.shortcut;
|
|
1772
|
+
btn.appendChild(sc);
|
|
1773
|
+
}
|
|
1774
|
+
if (it.submenu != null) {
|
|
1775
|
+
btn.setAttribute("aria-haspopup", "menu");
|
|
1776
|
+
btn.setAttribute("aria-expanded", "false");
|
|
1777
|
+
btn.classList.add("twm-menubar__item--submenu");
|
|
1778
|
+
const arrow = d.createElement("span");
|
|
1779
|
+
arrow.className = "twm-menubar__arrow";
|
|
1780
|
+
arrow.setAttribute("aria-hidden", "true");
|
|
1781
|
+
btn.appendChild(arrow);
|
|
1782
|
+
}
|
|
1783
|
+
this._models.set(btn, it);
|
|
1784
|
+
this._apply(btn, it);
|
|
1785
|
+
wrap.appendChild(btn);
|
|
1786
|
+
menu.appendChild(wrap);
|
|
1787
|
+
}
|
|
1788
|
+
return menu;
|
|
1789
|
+
}
|
|
1790
|
+
// ── activation ────────────────────────────────────────────────────
|
|
1791
|
+
_activate(btn) {
|
|
1792
|
+
const item = this._models.get(btn);
|
|
1793
|
+
if (!item || this._isDisabled(item)) return;
|
|
1794
|
+
if (item.submenu != null) {
|
|
1795
|
+
this._openSub(btn, "first");
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
this._exit();
|
|
1799
|
+
this._fire(item);
|
|
1800
|
+
}
|
|
1801
|
+
/** Flip a check/radio item and tell the embedder. The ONE door to
|
|
1802
|
+
* `onAction`, for clicks, keys and palette rows alike. */
|
|
1803
|
+
_fire(item) {
|
|
1804
|
+
if (!item || item.separator || item._empty || this._isDisabled(item) || item.submenu != null) return;
|
|
1805
|
+
let checked;
|
|
1806
|
+
if (_kind(item) === "menuitemradio") {
|
|
1807
|
+
this._check(item, item.id, true);
|
|
1808
|
+
checked = true;
|
|
1809
|
+
} else if (_kind(item) === "menuitemcheckbox") {
|
|
1810
|
+
checked = !this._isChecked(item);
|
|
1811
|
+
this._check(item, item.id, checked);
|
|
1812
|
+
}
|
|
1813
|
+
this._onAction?.(item.id, { item, checked });
|
|
1814
|
+
}
|
|
1815
|
+
// ── events ────────────────────────────────────────────────────────
|
|
1816
|
+
_onMouseDown(e) {
|
|
1817
|
+
if (e.target.closest?.(SEL_TITLE)) this._enter();
|
|
1818
|
+
e.preventDefault();
|
|
1819
|
+
}
|
|
1820
|
+
_onClick(e) {
|
|
1821
|
+
const title = e.target.closest?.(SEL_TITLE);
|
|
1822
|
+
if (title) {
|
|
1823
|
+
const i = this._titles.indexOf(title);
|
|
1824
|
+
this._enter();
|
|
1825
|
+
if (this._openIdx === i) this._closeAll();
|
|
1826
|
+
else this._open(i, null);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
const btn = e.target.closest?.(SEL_ITEM);
|
|
1830
|
+
if (btn) {
|
|
1831
|
+
e.stopPropagation();
|
|
1832
|
+
this._activate(btn);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
_onMouseOver(e) {
|
|
1836
|
+
const title = e.target.closest?.(SEL_TITLE);
|
|
1837
|
+
if (title) {
|
|
1838
|
+
const i = this._titles.indexOf(title);
|
|
1839
|
+
if (this._openIdx >= 0 && this._openIdx !== i) this._open(i, null);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
const btn = e.target.closest?.(SEL_ITEM);
|
|
1843
|
+
if (!btn) return;
|
|
1844
|
+
const item = this._models.get(btn);
|
|
1845
|
+
if (!item || this._isDisabled(item)) return;
|
|
1846
|
+
btn.focus({ preventScroll: true });
|
|
1847
|
+
const level = this._levelOf(btn);
|
|
1848
|
+
if (item.submenu != null) this._openSub(btn, null);
|
|
1849
|
+
else if (level >= 0) this._closeFrom(level + 1);
|
|
1850
|
+
}
|
|
1851
|
+
/** Tabbed into: what had focus is the event's `relatedTarget`. */
|
|
1852
|
+
_onFocusIn(e) {
|
|
1853
|
+
if (!this._active) this._enter(e.relatedTarget);
|
|
1854
|
+
}
|
|
1855
|
+
/** Focus left the bar: to an element outside it, or to nothing (a press
|
|
1856
|
+
* on a non-focusable area, the window losing focus). Either way the
|
|
1857
|
+
* activation ends, so the next Alt/F10 remembers what has focus THEN. */
|
|
1858
|
+
_onFocusOut(e) {
|
|
1859
|
+
const to = e.relatedTarget;
|
|
1860
|
+
if (!to || !this.el.contains(to)) this._deactivate();
|
|
1861
|
+
}
|
|
1862
|
+
_onKey(e) {
|
|
1863
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
1864
|
+
const title = e.target.closest?.(SEL_TITLE);
|
|
1865
|
+
const btn = title ? null : e.target.closest?.(SEL_ITEM);
|
|
1866
|
+
if (!title && !btn) return;
|
|
1867
|
+
if (e.key === "Tab") {
|
|
1868
|
+
this._deactivate();
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
const handled = title ? this._titleKey(e.key, this._titles.indexOf(title)) : this._itemKey(e.key, btn);
|
|
1872
|
+
if (handled) e.preventDefault();
|
|
1873
|
+
if (handled || !/^F\d+$/.test(e.key)) e.stopPropagation();
|
|
1874
|
+
}
|
|
1875
|
+
_titleKey(key, i) {
|
|
1876
|
+
const n = this._titles.length;
|
|
1877
|
+
const open = this._openIdx >= 0;
|
|
1878
|
+
switch (key) {
|
|
1879
|
+
case "ArrowRight":
|
|
1880
|
+
open ? this._open(i + 1, null) : this._focusTitle((i + 1) % n);
|
|
1881
|
+
return true;
|
|
1882
|
+
case "ArrowLeft":
|
|
1883
|
+
open ? this._open(i - 1, null) : this._focusTitle((i - 1 + n) % n);
|
|
1884
|
+
return true;
|
|
1885
|
+
case "Home":
|
|
1886
|
+
open ? this._open(0, null) : this._focusTitle(0);
|
|
1887
|
+
return true;
|
|
1888
|
+
case "End":
|
|
1889
|
+
open ? this._open(n - 1, null) : this._focusTitle(n - 1);
|
|
1890
|
+
return true;
|
|
1891
|
+
case "ArrowDown":
|
|
1892
|
+
case "Enter":
|
|
1893
|
+
case " ":
|
|
1894
|
+
if (open && this._openIdx === i) this._focusIn(this._stack[0].menu, "first");
|
|
1895
|
+
else this._open(i, "first");
|
|
1896
|
+
return true;
|
|
1897
|
+
case "ArrowUp":
|
|
1898
|
+
if (open && this._openIdx === i) this._focusIn(this._stack[0].menu, "last");
|
|
1899
|
+
else this._open(i, "last");
|
|
1900
|
+
return true;
|
|
1901
|
+
case "Escape":
|
|
1902
|
+
if (open) this._closeAll();
|
|
1903
|
+
else this._exit();
|
|
1904
|
+
return true;
|
|
1905
|
+
default:
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
_itemKey(key, btn) {
|
|
1910
|
+
const level = this._levelOf(btn);
|
|
1911
|
+
if (level < 0) return false;
|
|
1912
|
+
const menu = this._stack[level].menu;
|
|
1913
|
+
const item = this._models.get(btn);
|
|
1914
|
+
const go = (b) => b?.focus({ preventScroll: true });
|
|
1915
|
+
if ((key === "ArrowLeft" || key === "Escape") && this._stack[level + 1]?.parent === btn) {
|
|
1916
|
+
this._closeFrom(level + 1);
|
|
1917
|
+
return true;
|
|
1918
|
+
}
|
|
1919
|
+
if (key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End") {
|
|
1920
|
+
this._closeFrom(level + 1);
|
|
1921
|
+
}
|
|
1922
|
+
switch (key) {
|
|
1923
|
+
case "ArrowDown":
|
|
1924
|
+
go(this._stepFrom(menu, btn, 1));
|
|
1925
|
+
return true;
|
|
1926
|
+
case "ArrowUp":
|
|
1927
|
+
go(this._stepFrom(menu, btn, -1));
|
|
1928
|
+
return true;
|
|
1929
|
+
case "Home":
|
|
1930
|
+
this._focusIn(menu, "first");
|
|
1931
|
+
return true;
|
|
1932
|
+
case "End":
|
|
1933
|
+
this._focusIn(menu, "last");
|
|
1934
|
+
return true;
|
|
1935
|
+
case "ArrowRight":
|
|
1936
|
+
if (item?.submenu != null && !this._isDisabled(item)) this._openSub(btn, "first");
|
|
1937
|
+
else this._open(this._openIdx + 1, "first");
|
|
1938
|
+
return true;
|
|
1939
|
+
case "ArrowLeft":
|
|
1940
|
+
if (level > 0) this._closeFrom(level);
|
|
1941
|
+
else this._open(this._openIdx - 1, "first");
|
|
1942
|
+
return true;
|
|
1943
|
+
case "Enter":
|
|
1944
|
+
case " ":
|
|
1945
|
+
this._activate(btn);
|
|
1946
|
+
return true;
|
|
1947
|
+
case "Escape":
|
|
1948
|
+
if (level > 0) this._closeFrom(level);
|
|
1949
|
+
else this._closeAll();
|
|
1950
|
+
return true;
|
|
1951
|
+
default:
|
|
1952
|
+
return false;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
/** Alt on its own, and F10: armed on keydown, acted on here or on keyup. */
|
|
1956
|
+
_onDocKeyDown(e) {
|
|
1957
|
+
if (e.key === "Alt") {
|
|
1958
|
+
this._altArmed = !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.repeat;
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
this._altArmed = false;
|
|
1962
|
+
if (e.key === "F10" && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
|
1963
|
+
e.preventDefault();
|
|
1964
|
+
e.stopPropagation();
|
|
1965
|
+
this._toggle();
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
_onDocKeyUp(e) {
|
|
1969
|
+
if (e.key !== "Alt") return;
|
|
1970
|
+
const armed = this._altArmed;
|
|
1971
|
+
this._altArmed = false;
|
|
1972
|
+
if (!armed) return;
|
|
1973
|
+
e.preventDefault();
|
|
1974
|
+
this._toggle();
|
|
1975
|
+
}
|
|
1976
|
+
_onDocMouseDown(e) {
|
|
1977
|
+
this._altArmed = false;
|
|
1978
|
+
if (this._openIdx >= 0 && !this.el.contains(e.target)) this._deactivate();
|
|
1979
|
+
}
|
|
1980
|
+
_toggle() {
|
|
1981
|
+
if (this._active && (this._openIdx >= 0 || this.el.contains(this._doc.activeElement))) this._exit();
|
|
1982
|
+
else this.focus();
|
|
1983
|
+
}
|
|
1984
|
+
};
|
|
1985
|
+
function _resolve(items) {
|
|
1986
|
+
const v = typeof items === "function" ? items() : items;
|
|
1987
|
+
return Array.isArray(v) ? v : [];
|
|
1988
|
+
}
|
|
1989
|
+
function _kind(item) {
|
|
1990
|
+
if (item.radio != null) return "menuitemradio";
|
|
1991
|
+
if (typeof item.checked === "boolean") return "menuitemcheckbox";
|
|
1992
|
+
return "menuitem";
|
|
1993
|
+
}
|
|
1994
|
+
function _validateMenus(menus) {
|
|
1995
|
+
if (!Array.isArray(menus) || !menus.length) {
|
|
1996
|
+
throw new TypeError("createMenuBar: `menus` must be a non-empty array");
|
|
1997
|
+
}
|
|
1998
|
+
const walk = (items, where) => {
|
|
1999
|
+
if (typeof items === "function") return;
|
|
2000
|
+
if (!Array.isArray(items)) throw new TypeError(`createMenuBar: ${where} needs \`items\` (an array or a function)`);
|
|
2001
|
+
for (const it of items) {
|
|
2002
|
+
if (it?.separator) continue;
|
|
2003
|
+
if (it?.id == null || it?.label == null) {
|
|
2004
|
+
throw new TypeError(`createMenuBar: an item in ${where} needs an id and a label`);
|
|
2005
|
+
}
|
|
2006
|
+
if (it.submenu != null) walk(it.submenu, `"${it.label}"`);
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
for (const m of menus) {
|
|
2010
|
+
if (m?.id == null || m?.label == null) throw new TypeError("createMenuBar: a menu needs an id and a label");
|
|
2011
|
+
walk(m.items, `menu "${m.label}"`);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
function _place(menu, anchor, side) {
|
|
2015
|
+
const view = menu.ownerDocument.defaultView;
|
|
2016
|
+
const a = anchor.getBoundingClientRect();
|
|
2017
|
+
const r = menu.getBoundingClientRect();
|
|
2018
|
+
const vw = view?.innerWidth || 0;
|
|
2019
|
+
const vh = view?.innerHeight || 0;
|
|
2020
|
+
let left = side === "below" ? a.left : a.right;
|
|
2021
|
+
let top = side === "below" ? a.bottom : a.top;
|
|
2022
|
+
if (vw && left + r.width > vw) left = side === "below" ? vw - r.width - 4 : a.left - r.width;
|
|
2023
|
+
if (vh && top + r.height > vh) top = Math.max(0, vh - r.height - 4);
|
|
2024
|
+
menu.style.left = `${Math.max(0, left)}px`;
|
|
2025
|
+
menu.style.top = `${Math.max(0, top)}px`;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
export {
|
|
2029
|
+
setHelpProvider,
|
|
2030
|
+
helpProvider,
|
|
2031
|
+
helpCategories,
|
|
2032
|
+
helpCopy,
|
|
2033
|
+
renderMarkdown,
|
|
2034
|
+
DocumentOutline,
|
|
2035
|
+
createDocumentOutline,
|
|
2036
|
+
HelpModal,
|
|
2037
|
+
installHelpShortcut,
|
|
2038
|
+
createMenuBar,
|
|
2039
|
+
MenuBar
|
|
2040
|
+
};
|
|
2041
|
+
//# sourceMappingURL=chunk-JKZLNKG6.js.map
|