@mdzip/editor 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/README.md +73 -9
- package/dist/asset-cache.d.ts.map +1 -1
- package/dist/asset-cache.js +3 -7
- package/dist/asset-cache.js.map +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +2 -3
- package/dist/diff.js.map +1 -1
- package/dist/front-matter-extension.d.ts.map +1 -1
- package/dist/front-matter-extension.js +3 -1
- package/dist/front-matter-extension.js.map +1 -1
- package/dist/icons/page-width.d.ts +6 -0
- package/dist/icons/page-width.d.ts.map +1 -0
- package/dist/icons/page-width.js +15 -0
- package/dist/icons/page-width.js.map +1 -0
- package/dist/library-info.d.ts +7 -2
- package/dist/library-info.d.ts.map +1 -1
- package/dist/library-info.js +8 -2
- package/dist/library-info.js.map +1 -1
- package/dist/mermaid.d.ts +32 -0
- package/dist/mermaid.d.ts.map +1 -1
- package/dist/mermaid.js +127 -27
- package/dist/mermaid.js.map +1 -1
- package/dist/rendering.d.ts +12 -0
- package/dist/rendering.d.ts.map +1 -1
- package/dist/rendering.js +129 -4
- package/dist/rendering.js.map +1 -1
- package/dist/sha256.d.ts +10 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +83 -0
- package/dist/sha256.js.map +1 -0
- package/dist/view-css.d.ts.map +1 -1
- package/dist/view-css.js +2233 -2114
- package/dist/view-css.js.map +1 -1
- package/dist/view.d.ts +228 -20
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +1200 -415
- package/dist/view.js.map +1 -1
- package/dist/workspace.d.ts +12 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +22 -2
- package/dist/workspace.js.map +1 -1
- package/package.json +5 -4
package/dist/view.js
CHANGED
|
@@ -5,12 +5,13 @@ import { closeSearchPanel, openSearchPanel, search, searchKeymap } from '@codemi
|
|
|
5
5
|
import { Compartment, EditorState, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
|
6
6
|
import { Decoration, EditorView, MatchDecorator, ViewPlugin, WidgetType, dropCursor, keymap, lineNumbers } from '@codemirror/view';
|
|
7
7
|
import { tags } from '@lezer/highlight';
|
|
8
|
-
import { Bold, Check, ChevronDown, ChevronRight, ClipboardPaste, ClipboardType, Code, Columns2, Copy, Eraser, Eye, File, FileBraces, FileImage, Folder, FolderOpen, Hash, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Highlighter, ImagePlus, Info, Italic, Link2Off, Link, List, ListOrdered, CornerDownLeft, Moon, PackagePlus, PanelLeft, Pilcrow, Quote, Save, Scissors, Search, SpellCheck, SquareCode, SquarePen, Strikethrough, Sun, TextSelect, ZoomIn } from 'lucide';
|
|
8
|
+
import { Bold, Check, ChevronDown, ChevronRight, ClipboardPaste, ClipboardType, Code, Columns2, Copy, Eraser, Eye, File, FileBraces, FileImage, Folder, FolderOpen, Hash, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Highlighter, ImagePlus, Info, Italic, Link2Off, Link, List, ListOrdered, CornerDownLeft, Monitor, Moon, PackagePlus, PanelLeft, Pilcrow, Quote, Save, Scissors, Search, SpellCheck, SquareCode, SquarePen, Strikethrough, Sun, TextSelect, ZoomIn } from 'lucide';
|
|
9
9
|
import { samePath } from '@mdzip/core-js';
|
|
10
10
|
import { browserClipboardHasImage, readBrowserClipboardImage } from './browser.js';
|
|
11
11
|
import { buildPackedArchiveBytes, isMarkdownArchivePath, resolveMarkdownEntryPoint } from './archive-utils.js';
|
|
12
12
|
import { MdzipAssetSession, mdzipArchiveSourceId, sniffImageSize } from './asset-cache.js';
|
|
13
13
|
import { MD_MARKDOWN_ICON } from './icons/md-markdown.js';
|
|
14
|
+
import { PAGE_WIDTH_DEFAULT_ICON, PAGE_WIDTH_FULL_ICON, PAGE_WIDTH_NARROW_ICON, PAGE_WIDTH_WIDE_ICON } from './icons/page-width.js';
|
|
14
15
|
import { MDZIP_RUNTIME_LIBRARIES } from './library-info.js';
|
|
15
16
|
import { MdzipWorkspaceService, extensionForMime, normalizeArchivePath, relativeArchivePath } from './workspace.js';
|
|
16
17
|
import { buildMdzipNavTree, canEditMdzipPath, escapeHtml, isOrphanedMdzipAsset, mdzipEntryIconKind, isMdzipManifestPath, isMdzipDefaultPolicyExternalLink, isMdzipExternalLink, isMdzipWorkspaceRelativeLink, parseMdzipLink, renderMdzipPreviewHtml } from './workspace-view.js';
|
|
@@ -53,6 +54,11 @@ const SEARCH_ICON_HTML = lucideIcon(Search, TOOLBAR_ICON_CLASS);
|
|
|
53
54
|
const ZOOM_ICON_HTML = lucideIcon(ZoomIn, TOOLBAR_ICON_CLASS);
|
|
54
55
|
const DARK_THEME_ICON_HTML = lucideIcon(Moon, TOOLBAR_ICON_CLASS);
|
|
55
56
|
const LIGHT_THEME_ICON_HTML = lucideIcon(Sun, TOOLBAR_ICON_CLASS);
|
|
57
|
+
const SYSTEM_THEME_ICON_HTML = lucideIcon(Monitor, TOOLBAR_ICON_CLASS);
|
|
58
|
+
const PAGE_WIDTH_NARROW_ICON_HTML = lucideIcon(PAGE_WIDTH_NARROW_ICON, TOOLBAR_ICON_CLASS);
|
|
59
|
+
const PAGE_WIDTH_DEFAULT_ICON_HTML = lucideIcon(PAGE_WIDTH_DEFAULT_ICON, TOOLBAR_ICON_CLASS);
|
|
60
|
+
const PAGE_WIDTH_WIDE_ICON_HTML = lucideIcon(PAGE_WIDTH_WIDE_ICON, TOOLBAR_ICON_CLASS);
|
|
61
|
+
const PAGE_WIDTH_FULL_ICON_HTML = lucideIcon(PAGE_WIDTH_FULL_ICON, TOOLBAR_ICON_CLASS);
|
|
56
62
|
const FORMAT_ICON_CLASS = 'format-icon';
|
|
57
63
|
const BOLD_ICON_HTML = lucideIcon(Bold, FORMAT_ICON_CLASS);
|
|
58
64
|
const ITALIC_ICON_HTML = lucideIcon(Italic, FORMAT_ICON_CLASS);
|
|
@@ -152,7 +158,8 @@ function renderContextMenuItem(item) {
|
|
|
152
158
|
? `<span class="nav-menu-shortcut">${escapeHtml(item.shortcut)}</span>`
|
|
153
159
|
: '';
|
|
154
160
|
const disabledAttrs = item.disabled ? ' disabled aria-disabled="true"' : '';
|
|
155
|
-
|
|
161
|
+
const tooltipAttr = item.tooltip ? ` data-tooltip="${escapeHtml(item.tooltip)}"` : '';
|
|
162
|
+
return `<button type="button" role="menuitem" data-menu-action="${escapeHtml(item.action)}"${disabledAttrs}${tooltipAttr}>${icon}${label}${shortcut}</button>`;
|
|
156
163
|
}
|
|
157
164
|
function renderContextMenuItems(items) {
|
|
158
165
|
return items
|
|
@@ -164,11 +171,28 @@ const PREVIEW_MAX_WIDTH_ALIASES = {
|
|
|
164
171
|
default: 900,
|
|
165
172
|
wide: 1200,
|
|
166
173
|
};
|
|
167
|
-
|
|
168
|
-
|
|
174
|
+
/** The CSS value for --mdzip-preview-content-max-width, or undefined for the built-in default. */
|
|
175
|
+
function previewMaxWidthCss(value) {
|
|
176
|
+
if (value === undefined || value === 'default')
|
|
169
177
|
return undefined;
|
|
170
|
-
|
|
178
|
+
if (typeof value === 'number')
|
|
179
|
+
return `${value}px`;
|
|
180
|
+
if (value === 'full')
|
|
181
|
+
return 'none';
|
|
182
|
+
return `calc(${PREVIEW_MAX_WIDTH_ALIASES[value]}px * var(--mdz-zoom, 1))`;
|
|
171
183
|
}
|
|
184
|
+
const PREVIEW_WIDTH_LABELS = {
|
|
185
|
+
narrow: 'Narrow',
|
|
186
|
+
default: 'Default',
|
|
187
|
+
wide: 'Wide',
|
|
188
|
+
full: 'Full Width'
|
|
189
|
+
};
|
|
190
|
+
const PREVIEW_WIDTH_ICONS_HTML = {
|
|
191
|
+
narrow: PAGE_WIDTH_NARROW_ICON_HTML,
|
|
192
|
+
default: PAGE_WIDTH_DEFAULT_ICON_HTML,
|
|
193
|
+
wide: PAGE_WIDTH_WIDE_ICON_HTML,
|
|
194
|
+
full: PAGE_WIDTH_FULL_ICON_HTML
|
|
195
|
+
};
|
|
172
196
|
const ALL_HEADINGS = [1, 2, 3, 4, 5, 6];
|
|
173
197
|
const ALL_LAYOUT_CONTROLS = {
|
|
174
198
|
source: true,
|
|
@@ -219,6 +243,7 @@ const CONTROL_PRESETS = {
|
|
|
219
243
|
lineNumbers: false,
|
|
220
244
|
save: false,
|
|
221
245
|
zoom: false,
|
|
246
|
+
previewWidth: false,
|
|
222
247
|
colorScheme: false,
|
|
223
248
|
orphanActions: false,
|
|
224
249
|
fileActions: false,
|
|
@@ -236,6 +261,7 @@ const CONTROL_PRESETS = {
|
|
|
236
261
|
lineNumbers: true,
|
|
237
262
|
save: false,
|
|
238
263
|
zoom: true,
|
|
264
|
+
previewWidth: true,
|
|
239
265
|
colorScheme: true,
|
|
240
266
|
orphanActions: false,
|
|
241
267
|
fileActions: false,
|
|
@@ -253,6 +279,7 @@ const CONTROL_PRESETS = {
|
|
|
253
279
|
lineNumbers: true,
|
|
254
280
|
save: true,
|
|
255
281
|
zoom: true,
|
|
282
|
+
previewWidth: true,
|
|
256
283
|
colorScheme: true,
|
|
257
284
|
orphanActions: true,
|
|
258
285
|
fileActions: true,
|
|
@@ -270,6 +297,7 @@ const CONTROL_PRESETS = {
|
|
|
270
297
|
lineNumbers: true,
|
|
271
298
|
save: false,
|
|
272
299
|
zoom: true,
|
|
300
|
+
previewWidth: true,
|
|
273
301
|
colorScheme: true,
|
|
274
302
|
orphanActions: true,
|
|
275
303
|
fileActions: true,
|
|
@@ -632,6 +660,44 @@ const noSpellcheckHighlight = ViewPlugin.fromClass(class {
|
|
|
632
660
|
// createCmEditor. Fully inert unless a host imageEditHandler is set — see
|
|
633
661
|
// that click handler's own gate.
|
|
634
662
|
const IMAGE_EDIT_AFFORDANCE_CLICK_EVENT = 'mdzip-image-edit-affordance-click';
|
|
663
|
+
/** Holds an archive image's original `src` in the preview, whose `src` becomes a blob: URL. */
|
|
664
|
+
const PREVIEW_IMAGE_SOURCE_ATTRIBUTE = 'data-mdzip-source';
|
|
665
|
+
// Preview chrome the editor adds around rendered content: removed, or
|
|
666
|
+
// unwrapped (children kept), when the selection is copied as HTML.
|
|
667
|
+
const PREVIEW_CHROME_REMOVE_SELECTOR = [
|
|
668
|
+
'.mdzip-code-block-header',
|
|
669
|
+
'.mdzip-chunk-sentinel',
|
|
670
|
+
'[data-mdzip-image-edit-affordance]'
|
|
671
|
+
].join(',');
|
|
672
|
+
// Subtrees left out of a plain-text copy: the chrome above, plus markup whose
|
|
673
|
+
// text isn't document text (a drawn diagram's SVG carries its stylesheet).
|
|
674
|
+
const PLAIN_TEXT_SKIP_SELECTOR = `${PREVIEW_CHROME_REMOVE_SELECTOR},style,script,template,svg,button`;
|
|
675
|
+
/**
|
|
676
|
+
* The text of rendered preview markup for a plain-text copy: the document's
|
|
677
|
+
* own text, without style or script contents, drawn diagrams or the editor's
|
|
678
|
+
* chrome. Walks the DOM (no clone), so it's cheap on a large live preview.
|
|
679
|
+
*/
|
|
680
|
+
function previewPlainText(root) {
|
|
681
|
+
const parts = [];
|
|
682
|
+
// NodeFilter: SHOW_ELEMENT | SHOW_TEXT; FILTER_ACCEPT 1, REJECT 2 (skips the subtree), SKIP 3.
|
|
683
|
+
const walker = root.ownerDocument.createTreeWalker(root, 0x1 | 0x4, {
|
|
684
|
+
acceptNode: (node) => node.nodeType === 1
|
|
685
|
+
? (node.matches(PLAIN_TEXT_SKIP_SELECTOR) ? 2 : 3)
|
|
686
|
+
: 1
|
|
687
|
+
});
|
|
688
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
689
|
+
parts.push(node.data);
|
|
690
|
+
}
|
|
691
|
+
return parts.join('');
|
|
692
|
+
}
|
|
693
|
+
const PLAIN_TEXT_TOOLTIP = 'Text only, for Notepad or a code editor';
|
|
694
|
+
const FORMATTED_TEXT_TOOLTIP = 'Headings, styles and images, for Word, Google Docs or email';
|
|
695
|
+
const PREVIEW_CHROME_UNWRAP_SELECTOR = [
|
|
696
|
+
'.mdzip-chunk',
|
|
697
|
+
'.mdzip-code-block',
|
|
698
|
+
'.mdzip-code-block-body',
|
|
699
|
+
'.mdzip-image-slot'
|
|
700
|
+
].join(',');
|
|
635
701
|
const imageEditAffordanceEffect = StateEffect.define();
|
|
636
702
|
class ImageEditAffordanceWidget extends WidgetType {
|
|
637
703
|
constructor(from, to) {
|
|
@@ -695,6 +761,14 @@ function injectStyles(doc) {
|
|
|
695
761
|
style.textContent = WORKSPACE_CSS;
|
|
696
762
|
(doc.head ?? doc.documentElement).appendChild(style);
|
|
697
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* Sets a control's tooltip after startup. prepareTooltips() turns `title` into
|
|
766
|
+
* `data-tooltip` only once, so a later `title` would show a stale tooltip.
|
|
767
|
+
*/
|
|
768
|
+
function setTooltip(element, text) {
|
|
769
|
+
element.dataset['tooltip'] = text;
|
|
770
|
+
element.removeAttribute('title');
|
|
771
|
+
}
|
|
698
772
|
function lucideIcon(icon, className) {
|
|
699
773
|
const classAttr = className ? ` class="${escapeHtml(className)}"` : '';
|
|
700
774
|
const children = icon
|
|
@@ -755,19 +829,19 @@ depth = 0) {
|
|
|
755
829
|
: isImageFile(node.entry.path)
|
|
756
830
|
? IMAGE_ICON_HTML
|
|
757
831
|
: FILE_ICON_HTML;
|
|
758
|
-
const orphanBtnHtml = isOrphaned && options.allowOrphanActions ? `
|
|
759
|
-
<span class="nav-orphan-button" role="button" tabindex="0"
|
|
760
|
-
title="Orphaned asset" aria-label="Orphaned asset actions"
|
|
761
|
-
data-orphan-path="${safePath}">
|
|
762
|
-
${ORPHAN_ICON_HTML}
|
|
832
|
+
const orphanBtnHtml = isOrphaned && options.allowOrphanActions ? `
|
|
833
|
+
<span class="nav-orphan-button" role="button" tabindex="0"
|
|
834
|
+
title="Orphaned asset" aria-label="Orphaned asset actions"
|
|
835
|
+
data-orphan-path="${safePath}">
|
|
836
|
+
${ORPHAN_ICON_HTML}
|
|
763
837
|
</span>` : '';
|
|
764
838
|
const draggable = options.allowDrag && !isManifest;
|
|
765
|
-
return `<button type="button" class="${classes}" title="${title}"
|
|
766
|
-
data-nav-path="${safePath}" data-orphan="${isOrphaned ? 'true' : ''}"${draggable ? ' draggable="true"' : ''}>
|
|
767
|
-
${guides}<span class="nav-caret"></span>
|
|
768
|
-
<span class="nav-file-icon ${iconKind}">${iconHtml}</span>
|
|
769
|
-
${orphanBtnHtml}
|
|
770
|
-
<span class="nav-label">${safeName}</span>
|
|
839
|
+
return `<button type="button" class="${classes}" title="${title}"
|
|
840
|
+
data-nav-path="${safePath}" data-orphan="${isOrphaned ? 'true' : ''}"${draggable ? ' draggable="true"' : ''}>
|
|
841
|
+
${guides}<span class="nav-caret"></span>
|
|
842
|
+
<span class="nav-file-icon ${iconKind}">${iconHtml}</span>
|
|
843
|
+
${orphanBtnHtml}
|
|
844
|
+
<span class="nav-label">${safeName}</span>
|
|
771
845
|
</button>`;
|
|
772
846
|
}
|
|
773
847
|
const childTrail = depth === 0 ? [] : [...trail, !isLast];
|
|
@@ -775,14 +849,14 @@ depth = 0) {
|
|
|
775
849
|
.map((child, index) => renderNavNode(child, state, options, childTrail, index === node.children.length - 1, depth + 1))
|
|
776
850
|
.join('');
|
|
777
851
|
const pending = options.pendingFolders.has(node.path);
|
|
778
|
-
return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
|
|
779
|
-
<summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
|
|
780
|
-
${guides}<span class="nav-caret" aria-hidden="true"></span>
|
|
781
|
-
<span class="nav-folder-icon closed">${FOLDER_CLOSED_ICON_HTML}</span>
|
|
782
|
-
<span class="nav-folder-icon open">${FOLDER_OPEN_ICON_HTML}</span>
|
|
783
|
-
<span class="nav-label">${escapeHtml(node.name)}</span>
|
|
784
|
-
</summary>
|
|
785
|
-
<div class="nav-directory-children">${children}</div>
|
|
852
|
+
return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
|
|
853
|
+
<summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
|
|
854
|
+
${guides}<span class="nav-caret" aria-hidden="true"></span>
|
|
855
|
+
<span class="nav-folder-icon closed">${FOLDER_CLOSED_ICON_HTML}</span>
|
|
856
|
+
<span class="nav-folder-icon open">${FOLDER_OPEN_ICON_HTML}</span>
|
|
857
|
+
<span class="nav-label">${escapeHtml(node.name)}</span>
|
|
858
|
+
</summary>
|
|
859
|
+
<div class="nav-directory-children">${children}</div>
|
|
786
860
|
</details>`;
|
|
787
861
|
}
|
|
788
862
|
// Inserts view-local pending (not yet saved) folders into the nav tree so they
|
|
@@ -859,6 +933,28 @@ export class MdzipWorkspaceView {
|
|
|
859
933
|
this.navVisible = true;
|
|
860
934
|
this.zoom = 1;
|
|
861
935
|
this.zoomOpen = false;
|
|
936
|
+
this.themeMenuOpen = false;
|
|
937
|
+
this.widthMenuOpen = false;
|
|
938
|
+
/** Choice whose icon the page-width button shows, so render() only swaps it on change. */
|
|
939
|
+
this.widthMenuIconChoice = null;
|
|
940
|
+
/** Choice whose icon the theme menu button shows, so render() only swaps it on change. */
|
|
941
|
+
this.themeMenuIconPreference = null;
|
|
942
|
+
this.colorSchemePreference = 'system';
|
|
943
|
+
/** The host theme System follows instead of the OS setting (see setSystemColorScheme). */
|
|
944
|
+
this.systemColorSource = null;
|
|
945
|
+
/** The System item's label and icon last rendered, so render() only rewrites them on change. */
|
|
946
|
+
this.systemItemKey = null;
|
|
947
|
+
/** Re-resolves the scheme when the OS setting changes while 'system' is chosen. */
|
|
948
|
+
this.onSystemColorSchemeChange = () => {
|
|
949
|
+
if (this.colorSchemePreference !== 'system' || this.systemColorSource || this.destroyed) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const next = this.resolveColorScheme('system');
|
|
953
|
+
if (next !== this.colorScheme) {
|
|
954
|
+
this.colorScheme = next;
|
|
955
|
+
this.render();
|
|
956
|
+
}
|
|
957
|
+
};
|
|
862
958
|
this.titleDialogOpen = false;
|
|
863
959
|
this.metadataDialogOpen = false;
|
|
864
960
|
this.titleDraft = '';
|
|
@@ -1023,6 +1119,10 @@ export class MdzipWorkspaceView {
|
|
|
1023
1119
|
// True while onPreviewRendered runs — see scrollToAnchor().
|
|
1024
1120
|
this.firingPreviewRendered = false;
|
|
1025
1121
|
this.destroyed = false;
|
|
1122
|
+
/** Counts preview Ctrl+C copies, so a late image upgrade never overwrites a newer copy. */
|
|
1123
|
+
this.previewCopyId = 0;
|
|
1124
|
+
/** The preview Ctrl+A selection's boundaries; Ctrl+C copies the whole document while it's unchanged. */
|
|
1125
|
+
this.previewSelectAll = null;
|
|
1026
1126
|
// After an anchor scroll, content above the target can still change height
|
|
1027
1127
|
// (images loading or easing open, a diagram rendering) and push it out of
|
|
1028
1128
|
// view. This holds the target at the pane's top edge until the user
|
|
@@ -1051,6 +1151,8 @@ export class MdzipWorkspaceView {
|
|
|
1051
1151
|
this.previewMemo = null;
|
|
1052
1152
|
this.previewGeneration = 0;
|
|
1053
1153
|
this.previewAbort = null;
|
|
1154
|
+
/** Aborts an in-flight in-place re-theme (see rethemePreview). */
|
|
1155
|
+
this.rethemeAbort = null;
|
|
1054
1156
|
this.previewHandles = [];
|
|
1055
1157
|
// Whether the latest preview generation has finished mounting and hydrating
|
|
1056
1158
|
// its images; drives `whenRendered()` and gates the hydration callback.
|
|
@@ -1070,10 +1172,11 @@ export class MdzipWorkspaceView {
|
|
|
1070
1172
|
this.contentDensity = options.contentDensity ?? 'comfortable';
|
|
1071
1173
|
this.previewMaxWidth = options.previewMaxWidth;
|
|
1072
1174
|
this.layout = options.initialLayout ?? defaultLayoutForPolicy(this.controlPolicy);
|
|
1073
|
-
this.
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1175
|
+
this.systemColorSchemeQuery = container.ownerDocument.defaultView?.matchMedia?.('(prefers-color-scheme: dark)') ?? null;
|
|
1176
|
+
this.systemColorSchemeQuery?.addEventListener?.('change', this.onSystemColorSchemeChange);
|
|
1177
|
+
this.systemColorSource = options.systemColorScheme ?? null;
|
|
1178
|
+
this.colorSchemePreference = options.initialColorScheme ?? 'system';
|
|
1179
|
+
this.colorScheme = this.resolveColorScheme(this.colorSchemePreference);
|
|
1077
1180
|
this.navVisible = options.navigationButtonActive ?? this.navVisible;
|
|
1078
1181
|
this.markdownRenderer = options.markdownRenderer;
|
|
1079
1182
|
this.markdownExtensions = options.markdownExtensions ?? [];
|
|
@@ -1101,8 +1204,16 @@ export class MdzipWorkspaceView {
|
|
|
1101
1204
|
this.elSaveBtn = q('[data-ref="save-btn"]');
|
|
1102
1205
|
this.elSearchBtn = q('[data-ref="search-btn"]');
|
|
1103
1206
|
this.elZoomBtn = q('[data-ref="zoom-btn"]');
|
|
1207
|
+
this.elWidthControls = q('[data-ref="width-controls"]');
|
|
1208
|
+
this.elWidthMenuBtn = q('[data-ref="width-menu-btn"]');
|
|
1209
|
+
this.elWidthMenuIcon = q('[data-ref="width-menu-icon"]');
|
|
1210
|
+
this.elWidthPopover = q('[data-ref="width-popover"]');
|
|
1104
1211
|
this.elThemeControls = q('[data-ref="theme-controls"]');
|
|
1105
1212
|
this.elDarkThemeBtn = q('[data-ref="dark-theme-btn"]');
|
|
1213
|
+
this.elThemeMenuBtn = q('[data-ref="theme-menu-btn"]');
|
|
1214
|
+
this.elThemeMenuIcon = q('[data-ref="theme-menu-icon"]');
|
|
1215
|
+
this.elThemePopover = q('[data-ref="theme-popover"]');
|
|
1216
|
+
this.elSystemThemeBtn = q('[data-ref="system-theme-btn"]');
|
|
1106
1217
|
this.elLightThemeBtn = q('[data-ref="light-theme-btn"]');
|
|
1107
1218
|
this.elZoomPopover = q('[data-ref="zoom-popover"]');
|
|
1108
1219
|
this.elZoomLevel = q('[data-ref="zoom-level"]');
|
|
@@ -1453,6 +1564,7 @@ export class MdzipWorkspaceView {
|
|
|
1453
1564
|
this.cmEditor?.focus();
|
|
1454
1565
|
}
|
|
1455
1566
|
destroy() {
|
|
1567
|
+
this.systemColorSchemeQuery?.removeEventListener?.('change', this.onSystemColorSchemeChange);
|
|
1456
1568
|
this.conversionDocumentGeneration += 1;
|
|
1457
1569
|
this.resolveImageInsertDialog(null);
|
|
1458
1570
|
this.resolvePackFilesDialog(null);
|
|
@@ -1603,10 +1715,11 @@ export class MdzipWorkspaceView {
|
|
|
1603
1715
|
this.applyDensityClasses();
|
|
1604
1716
|
}
|
|
1605
1717
|
/**
|
|
1606
|
-
* Sets the preview reading-column width
|
|
1607
|
-
*
|
|
1608
|
-
*
|
|
1609
|
-
*
|
|
1718
|
+
* Sets the preview reading-column width, as the toolbar's page-width menu
|
|
1719
|
+
* does: `'narrow'`, `'default'`, `'wide'` (all scale with zoom) or `'full'`.
|
|
1720
|
+
* A number is an exact pixel width that doesn't scale with zoom, and leaves
|
|
1721
|
+
* no menu item checked. `undefined` is `'default'` (see
|
|
1722
|
+
* {@link MdzipPreviewMaxWidth}).
|
|
1610
1723
|
*/
|
|
1611
1724
|
setPreviewMaxWidth(value) {
|
|
1612
1725
|
if (value === this.previewMaxWidth) {
|
|
@@ -1693,6 +1806,8 @@ export class MdzipWorkspaceView {
|
|
|
1693
1806
|
resetPreviewState() {
|
|
1694
1807
|
this.previewAbort?.abort();
|
|
1695
1808
|
this.previewAbort = null;
|
|
1809
|
+
this.rethemeAbort?.abort();
|
|
1810
|
+
this.rethemeAbort = null;
|
|
1696
1811
|
this.previewGeneration += 1;
|
|
1697
1812
|
this.destroyPreviewHandles();
|
|
1698
1813
|
this.previewMemo = null;
|
|
@@ -1742,6 +1857,64 @@ export class MdzipWorkspaceView {
|
|
|
1742
1857
|
&& memo.text === snapshot.currentText
|
|
1743
1858
|
&& memo.colorScheme === this.colorScheme;
|
|
1744
1859
|
}
|
|
1860
|
+
/**
|
|
1861
|
+
* True when only the color scheme changed since `memo`, and the preview can
|
|
1862
|
+
* be kept: the default renderer (whose HTML doesn't depend on the scheme),
|
|
1863
|
+
* every extension that transforms or mounts content can re-theme in place,
|
|
1864
|
+
* and the current render has finished mounting (otherwise its chunks were
|
|
1865
|
+
* rendered, or are still rendering, with the old scheme).
|
|
1866
|
+
*/
|
|
1867
|
+
canRethemePreviewInPlace(memo, snapshot) {
|
|
1868
|
+
return memo.colorScheme !== this.colorScheme
|
|
1869
|
+
&& memo.path === snapshot.currentPath
|
|
1870
|
+
&& memo.pathType === snapshot.currentPathType
|
|
1871
|
+
&& memo.text === snapshot.currentText
|
|
1872
|
+
&& this.renderingService.supportsChunking
|
|
1873
|
+
&& this.previewMountedGeneration === this.previewGeneration
|
|
1874
|
+
&& !this.chunkedRenderState?.mounting
|
|
1875
|
+
&& this.markdownExtensions.every((extension) => !!extension.updateColorScheme
|
|
1876
|
+
|| !(extension.transformMarkdown || extension.transformHtml || extension.mount));
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Applies a scheme-only change without re-rendering: the root's theme class
|
|
1880
|
+
* (set in render()) re-colors the DOM; chunks not mounted yet will render
|
|
1881
|
+
* with the new scheme; cached chunk HTML (which may hold old-scheme diagram
|
|
1882
|
+
* SVG) is dropped; and each extension re-themes its mounted output.
|
|
1883
|
+
*/
|
|
1884
|
+
rethemePreview(snapshot, memo) {
|
|
1885
|
+
memo.colorScheme = this.colorScheme;
|
|
1886
|
+
if (this.chunkedRenderState) {
|
|
1887
|
+
this.chunkedRenderState.context = { ...this.chunkedRenderState.context, colorScheme: this.colorScheme };
|
|
1888
|
+
}
|
|
1889
|
+
this.chunkHtmlCache = null;
|
|
1890
|
+
if (snapshot.currentPathType !== 'markdown') {
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
this.rethemeAbort?.abort();
|
|
1894
|
+
const abort = new AbortController();
|
|
1895
|
+
this.rethemeAbort = abort;
|
|
1896
|
+
const context = this.createMarkdownContext(snapshot, abort.signal);
|
|
1897
|
+
const generation = this.previewGeneration;
|
|
1898
|
+
const report = (error) => {
|
|
1899
|
+
if (generation === this.previewGeneration && !abort.signal.aborted) {
|
|
1900
|
+
this.options.onFailed?.(error);
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
for (const extension of this.markdownExtensions) {
|
|
1904
|
+
if (!extension.updateColorScheme) {
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
try {
|
|
1908
|
+
const result = extension.updateColorScheme(this.elPreviewContent, context);
|
|
1909
|
+
if (isThenable(result)) {
|
|
1910
|
+
void Promise.resolve(result).catch(report);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
catch (error) {
|
|
1914
|
+
report(error);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1745
1918
|
updatePreview(snapshot, entryClaimed) {
|
|
1746
1919
|
this.lastContentChangeTime = performance.now();
|
|
1747
1920
|
// Captured before anything below can move it — see
|
|
@@ -1766,6 +1939,12 @@ export class MdzipWorkspaceView {
|
|
|
1766
1939
|
// mounted extension handles.
|
|
1767
1940
|
return;
|
|
1768
1941
|
}
|
|
1942
|
+
if (memo && this.canRethemePreviewInPlace(memo, snapshot)) {
|
|
1943
|
+
// Only the color scheme changed: CSS re-colors the existing DOM, and
|
|
1944
|
+
// extensions re-theme their own output, so nothing moves.
|
|
1945
|
+
this.rethemePreview(snapshot, memo);
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1769
1948
|
const animateImageHydration = this.shouldAnimateImageHydration(memo, snapshot);
|
|
1770
1949
|
// A same-document, text-only edit with an existing chunked structure can
|
|
1771
1950
|
// be reconciled against (only the chunks whose source actually changed
|
|
@@ -1789,6 +1968,8 @@ export class MdzipWorkspaceView {
|
|
|
1789
1968
|
const priorChunkedState = reconcileEligible ? this.chunkedRenderState : null;
|
|
1790
1969
|
this.previewAbort?.abort();
|
|
1791
1970
|
this.previewAbort = null;
|
|
1971
|
+
this.rethemeAbort?.abort();
|
|
1972
|
+
this.rethemeAbort = null;
|
|
1792
1973
|
if (!priorChunkedState) {
|
|
1793
1974
|
this.destroyPreviewHandles();
|
|
1794
1975
|
}
|
|
@@ -1960,6 +2141,8 @@ export class MdzipWorkspaceView {
|
|
|
1960
2141
|
}
|
|
1961
2142
|
continue;
|
|
1962
2143
|
}
|
|
2144
|
+
// The src is replaced by a blob: URL; Copy as HTML restores it from here.
|
|
2145
|
+
image.setAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE, source);
|
|
1963
2146
|
const known = this.assetSession.resolveKnownImage(source, context.currentPath);
|
|
1964
2147
|
if (known) {
|
|
1965
2148
|
if (known.width && known.height && !image.hasAttribute('width') && !image.hasAttribute('height')) {
|
|
@@ -2600,10 +2783,15 @@ export class MdzipWorkspaceView {
|
|
|
2600
2783
|
// Same race this field guards against everywhere else: an edit landing
|
|
2601
2784
|
// while this scroll-triggered continuation is mid-flight must not
|
|
2602
2785
|
// reconcile against a cursor that doesn't yet reflect it.
|
|
2786
|
+
// The live context, not the one captured when this sentinel was armed:
|
|
2787
|
+
// a color scheme change since then (rethemePreview) replaces it, and
|
|
2788
|
+
// chunks mounted from here on must render with the new scheme.
|
|
2789
|
+
let liveContext = context;
|
|
2603
2790
|
if (this.chunkedRenderState?.generation === generation) {
|
|
2604
2791
|
this.chunkedRenderState.mounting = true;
|
|
2792
|
+
liveContext = this.chunkedRenderState.context;
|
|
2605
2793
|
}
|
|
2606
|
-
void this.mountChunkBatch(records, nextCursor,
|
|
2794
|
+
void this.mountChunkBatch(records, nextCursor, liveContext, generation, animateImageHydration, () => { })
|
|
2607
2795
|
.then((newCursor) => {
|
|
2608
2796
|
// Record whatever this batch actually mounted regardless of
|
|
2609
2797
|
// whether a newer edit has since landed — recordChunkProgress's
|
|
@@ -2622,7 +2810,7 @@ export class MdzipWorkspaceView {
|
|
|
2622
2810
|
this.chunkedRenderState.mounting = false;
|
|
2623
2811
|
}
|
|
2624
2812
|
if (newCursor < records.length) {
|
|
2625
|
-
this.armChunkSentinel(records, newCursor,
|
|
2813
|
+
this.armChunkSentinel(records, newCursor, liveContext, generation, animateImageHydration);
|
|
2626
2814
|
}
|
|
2627
2815
|
});
|
|
2628
2816
|
};
|
|
@@ -3302,6 +3490,13 @@ export class MdzipWorkspaceView {
|
|
|
3302
3490
|
const showSaveControl = this.controlPolicy.save && snapshot.mode !== 'read-only';
|
|
3303
3491
|
const showZoomControl = this.controlPolicy.zoom;
|
|
3304
3492
|
const showColorSchemeControl = this.controlPolicy.colorScheme;
|
|
3493
|
+
// Only the rendered Markdown preview has a reading column to size, so the
|
|
3494
|
+
// menu is hidden in edit (source) layout and for images and other files.
|
|
3495
|
+
const showWidthControl = this.controlPolicy.previewWidth && !entryClaimed
|
|
3496
|
+
&& snapshot.currentPathType === 'markdown' && this.layout !== 'source';
|
|
3497
|
+
if (!showWidthControl) {
|
|
3498
|
+
this.widthMenuOpen = false;
|
|
3499
|
+
}
|
|
3305
3500
|
const showSearchControl = this.controlPolicy.search
|
|
3306
3501
|
&& canShowSource && snapshot.currentPathType === 'markdown';
|
|
3307
3502
|
const showEditControls = canEdit
|
|
@@ -3310,14 +3505,15 @@ export class MdzipWorkspaceView {
|
|
|
3310
3505
|
&& hasFormattingControls(this.controlPolicy.formatting);
|
|
3311
3506
|
const showToolbar = this.controlPolicy.toolbar
|
|
3312
3507
|
&& (showNavigationControl || showLayoutControls || showSaveControl
|
|
3313
|
-
|| showZoomControl ||
|
|
3508
|
+
|| showZoomControl || showWidthControl || showColorSchemeControl || showSearchControl
|
|
3509
|
+
|| showEditControls);
|
|
3314
3510
|
this.elDocumentStrip.hidden = !showTitleControl;
|
|
3315
3511
|
this.elToolbar.hidden = !showToolbar;
|
|
3316
3512
|
this.elToolbarLeft.hidden = !showNavigationControl;
|
|
3317
3513
|
this.elEditToolbar.hidden = !showEditControls;
|
|
3318
3514
|
this.elLayoutControls.hidden = !showLayoutControls;
|
|
3319
3515
|
this.elToolbarControls.hidden = !showSaveControl && !showZoomControl
|
|
3320
|
-
&& !showColorSchemeControl && !showSearchControl;
|
|
3516
|
+
&& !showWidthControl && !showColorSchemeControl && !showSearchControl;
|
|
3321
3517
|
this.elNavBtn.hidden = !showNavigationControl;
|
|
3322
3518
|
this.elPreviewBtn.hidden = !this.controlPolicy.layout.preview;
|
|
3323
3519
|
this.elSplitBtn.hidden = !this.controlPolicy.layout.split;
|
|
@@ -3325,16 +3521,17 @@ export class MdzipWorkspaceView {
|
|
|
3325
3521
|
this.elSaveBtn.hidden = !showSaveControl;
|
|
3326
3522
|
this.elSearchBtn.hidden = !showSearchControl;
|
|
3327
3523
|
this.elZoomBtn.hidden = !showZoomControl;
|
|
3524
|
+
this.elWidthControls.hidden = !showWidthControl;
|
|
3328
3525
|
this.elThemeControls.hidden = !showColorSchemeControl;
|
|
3329
3526
|
this.elRoot.style.setProperty('--mdz-zoom', String(this.zoom));
|
|
3330
3527
|
this.elRoot.style.setProperty('--nav-pane-width', `${this.navPaneWidth}px`);
|
|
3331
3528
|
this.elRoot.style.setProperty('--split-edit-ratio', String(this.splitRatio));
|
|
3332
|
-
const
|
|
3333
|
-
if (
|
|
3529
|
+
const previewMaxWidth = previewMaxWidthCss(this.previewMaxWidth);
|
|
3530
|
+
if (previewMaxWidth === undefined) {
|
|
3334
3531
|
this.elRoot.style.removeProperty('--mdzip-preview-content-max-width');
|
|
3335
3532
|
}
|
|
3336
3533
|
else {
|
|
3337
|
-
this.elRoot.style.setProperty('--mdzip-preview-content-max-width',
|
|
3534
|
+
this.elRoot.style.setProperty('--mdzip-preview-content-max-width', previewMaxWidth);
|
|
3338
3535
|
}
|
|
3339
3536
|
this.elRoot.classList.toggle('resizing', this.resizing);
|
|
3340
3537
|
this.elRoot.classList.toggle('mdzip-theme-dark', this.colorScheme === 'dark');
|
|
@@ -3394,10 +3591,53 @@ export class MdzipWorkspaceView {
|
|
|
3394
3591
|
this.elNavBtn.setAttribute('aria-pressed', String(this.navVisible));
|
|
3395
3592
|
}
|
|
3396
3593
|
this.elZoomBtn.classList.toggle('active', this.zoomOpen);
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3594
|
+
for (const [button, preference] of this.themeMenuItems()) {
|
|
3595
|
+
button.classList.toggle('active', this.colorSchemePreference === preference);
|
|
3596
|
+
button.setAttribute('aria-checked', String(this.colorSchemePreference === preference));
|
|
3597
|
+
}
|
|
3598
|
+
const systemIcon = this.systemColorSource?.icon
|
|
3599
|
+
? `<span class="${TOOLBAR_ICON_CLASS} mdzip-host-icon" aria-hidden="true">${this.systemColorSource.icon}</span>`
|
|
3600
|
+
: SYSTEM_THEME_ICON_HTML;
|
|
3601
|
+
const systemItemLabel = this.systemColorSource?.label ?? 'System setting';
|
|
3602
|
+
const systemItemKey = `${systemItemLabel}\n${systemIcon}`;
|
|
3603
|
+
if (this.systemItemKey !== systemItemKey) {
|
|
3604
|
+
this.systemItemKey = systemItemKey;
|
|
3605
|
+
this.themeMenuIconPreference = null;
|
|
3606
|
+
this.elSystemThemeBtn.innerHTML = systemIcon;
|
|
3607
|
+
setTooltip(this.elSystemThemeBtn, systemItemLabel);
|
|
3608
|
+
this.elSystemThemeBtn.setAttribute('aria-label', systemItemLabel);
|
|
3609
|
+
}
|
|
3610
|
+
if (this.themeMenuIconPreference !== this.colorSchemePreference) {
|
|
3611
|
+
this.themeMenuIconPreference = this.colorSchemePreference;
|
|
3612
|
+
this.elThemeMenuIcon.innerHTML = this.colorSchemePreference === 'system'
|
|
3613
|
+
? systemIcon
|
|
3614
|
+
: this.colorSchemePreference === 'dark' ? DARK_THEME_ICON_HTML : LIGHT_THEME_ICON_HTML;
|
|
3615
|
+
}
|
|
3616
|
+
const schemeLabel = this.colorSchemePreference === 'system'
|
|
3617
|
+
? `${this.systemColorSource?.label ?? 'System'} (${this.colorScheme})`
|
|
3618
|
+
: this.colorSchemePreference === 'dark' ? 'Dark' : 'Light';
|
|
3619
|
+
setTooltip(this.elThemeMenuBtn, `Color scheme: ${schemeLabel}`);
|
|
3620
|
+
this.elThemeMenuBtn.setAttribute('aria-label', `Color scheme: ${schemeLabel}`);
|
|
3621
|
+
this.elThemeMenuBtn.classList.toggle('active', this.themeMenuOpen);
|
|
3622
|
+
this.elThemeMenuBtn.setAttribute('aria-expanded', String(this.themeMenuOpen));
|
|
3623
|
+
this.elThemePopover.hidden = !this.themeMenuOpen;
|
|
3624
|
+
const widthChoice = this.previewWidthChoice();
|
|
3625
|
+
for (const [button, choice] of this.widthMenuItems()) {
|
|
3626
|
+
button.classList.toggle('active', widthChoice === choice);
|
|
3627
|
+
button.setAttribute('aria-checked', String(widthChoice === choice));
|
|
3628
|
+
}
|
|
3629
|
+
// A custom pixel width (set by the host) shows the default icon, with no item checked.
|
|
3630
|
+
const widthIconChoice = widthChoice ?? 'default';
|
|
3631
|
+
if (this.widthMenuIconChoice !== widthIconChoice) {
|
|
3632
|
+
this.widthMenuIconChoice = widthIconChoice;
|
|
3633
|
+
this.elWidthMenuIcon.innerHTML = PREVIEW_WIDTH_ICONS_HTML[widthIconChoice];
|
|
3634
|
+
}
|
|
3635
|
+
// Measuring the pane forces layout, so only while the menu is open; hover
|
|
3636
|
+
// and focus on the button refresh the labels too.
|
|
3637
|
+
this.updateWidthLabels(this.widthMenuOpen);
|
|
3638
|
+
this.elWidthMenuBtn.classList.toggle('active', this.widthMenuOpen);
|
|
3639
|
+
this.elWidthMenuBtn.setAttribute('aria-expanded', String(this.widthMenuOpen));
|
|
3640
|
+
this.elWidthPopover.hidden = !this.widthMenuOpen;
|
|
3401
3641
|
this.elZoomPopover.hidden = !this.zoomOpen;
|
|
3402
3642
|
this.elZoomLevel.textContent = `${Math.round(this.zoom * 100)}%`;
|
|
3403
3643
|
const showNavigationPane = this.controlPolicy.navigation
|
|
@@ -3565,8 +3805,10 @@ export class MdzipWorkspaceView {
|
|
|
3565
3805
|
const doc = this.elRoot.ownerDocument;
|
|
3566
3806
|
doc.addEventListener('click', () => {
|
|
3567
3807
|
this.closeFormatMenus();
|
|
3568
|
-
if (this.zoomOpen || this.contextMenuState) {
|
|
3808
|
+
if (this.zoomOpen || this.themeMenuOpen || this.widthMenuOpen || this.contextMenuState) {
|
|
3569
3809
|
this.zoomOpen = false;
|
|
3810
|
+
this.themeMenuOpen = false;
|
|
3811
|
+
this.widthMenuOpen = false;
|
|
3570
3812
|
this.contextMenuState = null;
|
|
3571
3813
|
this.render();
|
|
3572
3814
|
}
|
|
@@ -3575,6 +3817,12 @@ export class MdzipWorkspaceView {
|
|
|
3575
3817
|
if (e.key !== 'Escape') {
|
|
3576
3818
|
return;
|
|
3577
3819
|
}
|
|
3820
|
+
if (this.themeMenuOpen) {
|
|
3821
|
+
this.closeThemeMenu(true);
|
|
3822
|
+
}
|
|
3823
|
+
if (this.widthMenuOpen) {
|
|
3824
|
+
this.closeWidthMenu(true);
|
|
3825
|
+
}
|
|
3578
3826
|
if (this.contextMenuState || this.deleteDialogState || this.nameDialogState) {
|
|
3579
3827
|
this.contextMenuState = null;
|
|
3580
3828
|
this.deleteDialogState = null;
|
|
@@ -3583,11 +3831,12 @@ export class MdzipWorkspaceView {
|
|
|
3583
3831
|
}
|
|
3584
3832
|
});
|
|
3585
3833
|
// Ctrl/Cmd+A normally selects the whole page, which in split layout grabs
|
|
3586
|
-
// both panes' text at once.
|
|
3587
|
-
//
|
|
3588
|
-
//
|
|
3589
|
-
//
|
|
3590
|
-
//
|
|
3834
|
+
// both panes' text at once. With focus inside the preview pane (see the
|
|
3835
|
+
// mousedown handler below for how it gets there) it selects just the
|
|
3836
|
+
// rendered content; the source editor keeps its own defaultKeymap binding
|
|
3837
|
+
// since CodeMirror's content root sits outside this element. The
|
|
3838
|
+
// selection is remembered so the copy handler can copy the whole
|
|
3839
|
+
// document, including chunks not mounted yet, while it's unchanged.
|
|
3591
3840
|
doc.addEventListener('keydown', (e) => {
|
|
3592
3841
|
if (e.key.toLowerCase() !== 'a' || !(e.ctrlKey || e.metaKey) || e.shiftKey || e.altKey) {
|
|
3593
3842
|
return;
|
|
@@ -3596,7 +3845,8 @@ export class MdzipWorkspaceView {
|
|
|
3596
3845
|
return;
|
|
3597
3846
|
}
|
|
3598
3847
|
e.preventDefault();
|
|
3599
|
-
|
|
3848
|
+
this.selectAllPreview();
|
|
3849
|
+
this.keepPreviewSelectAll();
|
|
3600
3850
|
});
|
|
3601
3851
|
this.elNavBtn.addEventListener('click', () => {
|
|
3602
3852
|
if (!this.controlPolicy.navigation) {
|
|
@@ -3663,6 +3913,8 @@ export class MdzipWorkspaceView {
|
|
|
3663
3913
|
}
|
|
3664
3914
|
e.stopPropagation();
|
|
3665
3915
|
this.zoomOpen = !this.zoomOpen;
|
|
3916
|
+
this.themeMenuOpen = false;
|
|
3917
|
+
this.widthMenuOpen = false;
|
|
3666
3918
|
this.render();
|
|
3667
3919
|
});
|
|
3668
3920
|
this.elZoomPopover.addEventListener('click', (e) => e.stopPropagation());
|
|
@@ -3672,8 +3924,47 @@ export class MdzipWorkspaceView {
|
|
|
3672
3924
|
.addEventListener('click', () => this.setZoom(this.zoom + 0.1));
|
|
3673
3925
|
this.elZoomPopover.querySelector('[data-action="zoom-reset"]')
|
|
3674
3926
|
.addEventListener('click', () => this.setZoom(1));
|
|
3675
|
-
this.
|
|
3676
|
-
|
|
3927
|
+
this.elThemeMenuBtn.addEventListener('click', (e) => {
|
|
3928
|
+
e.stopPropagation();
|
|
3929
|
+
this.themeMenuOpen = !this.themeMenuOpen;
|
|
3930
|
+
this.zoomOpen = false;
|
|
3931
|
+
this.widthMenuOpen = false;
|
|
3932
|
+
this.render();
|
|
3933
|
+
// Keyboard activation (Enter/Space reports detail 0) moves focus into the
|
|
3934
|
+
// menu; a mouse click doesn't, so no focus tooltip covers the icons.
|
|
3935
|
+
if (this.themeMenuOpen && e.detail === 0) {
|
|
3936
|
+
this.themeMenuItems().find(([, preference]) => preference === this.colorSchemePreference)?.[0].focus();
|
|
3937
|
+
}
|
|
3938
|
+
});
|
|
3939
|
+
this.elThemePopover.addEventListener('click', (e) => e.stopPropagation());
|
|
3940
|
+
this.elThemePopover.addEventListener('keydown', (e) => {
|
|
3941
|
+
this.moveMenuFocus(e, this.themeMenuItems().map(([button]) => button));
|
|
3942
|
+
});
|
|
3943
|
+
for (const [button, preference] of this.themeMenuItems()) {
|
|
3944
|
+
button.addEventListener('click', () => this.chooseColorSchemeFromMenu(preference));
|
|
3945
|
+
}
|
|
3946
|
+
this.elWidthMenuBtn.addEventListener('click', (e) => {
|
|
3947
|
+
e.stopPropagation();
|
|
3948
|
+
this.widthMenuOpen = !this.widthMenuOpen;
|
|
3949
|
+
this.zoomOpen = false;
|
|
3950
|
+
this.themeMenuOpen = false;
|
|
3951
|
+
this.render();
|
|
3952
|
+
// As with the theme menu, only keyboard activation moves focus inside.
|
|
3953
|
+
if (this.widthMenuOpen && e.detail === 0) {
|
|
3954
|
+
const items = this.widthMenuItems();
|
|
3955
|
+
const choice = this.previewWidthChoice() ?? 'default';
|
|
3956
|
+
(items.find(([, item]) => item === choice) ?? items[0])[0].focus();
|
|
3957
|
+
}
|
|
3958
|
+
});
|
|
3959
|
+
this.elWidthMenuBtn.addEventListener('pointerenter', () => this.updateWidthLabels(true));
|
|
3960
|
+
this.elWidthMenuBtn.addEventListener('focus', () => this.updateWidthLabels(true));
|
|
3961
|
+
this.elWidthPopover.addEventListener('click', (e) => e.stopPropagation());
|
|
3962
|
+
this.elWidthPopover.addEventListener('keydown', (e) => {
|
|
3963
|
+
this.moveMenuFocus(e, this.widthMenuItems().map(([button]) => button));
|
|
3964
|
+
});
|
|
3965
|
+
for (const [button, choice] of this.widthMenuItems()) {
|
|
3966
|
+
button.addEventListener('click', () => this.choosePreviewWidthFromMenu(choice));
|
|
3967
|
+
}
|
|
3677
3968
|
this.elEditToolbar.addEventListener('click', (event) => {
|
|
3678
3969
|
const menuToggle = event.target
|
|
3679
3970
|
.closest('[data-format-menu-toggle]');
|
|
@@ -3975,6 +4266,41 @@ export class MdzipWorkspaceView {
|
|
|
3975
4266
|
e.stopPropagation();
|
|
3976
4267
|
this.render();
|
|
3977
4268
|
});
|
|
4269
|
+
// Ctrl+C on a preview selection copies Formatted Text, like the menu item:
|
|
4270
|
+
// clean markup and plain text now (the copy event is synchronous), then,
|
|
4271
|
+
// when the selection has images, the same copy again with them embedded,
|
|
4272
|
+
// since the preview's own blob: URLs only resolve inside this view.
|
|
4273
|
+
doc.addEventListener('copy', (e) => {
|
|
4274
|
+
const selection = doc.defaultView?.getSelection();
|
|
4275
|
+
if (this.destroyed || !e.clipboardData || !selection || selection.isCollapsed || selection.rangeCount === 0
|
|
4276
|
+
|| !this.elPreviewContent.contains(selection.anchorNode)
|
|
4277
|
+
|| this.workspace?.snapshot().currentPathType !== 'markdown') {
|
|
4278
|
+
return;
|
|
4279
|
+
}
|
|
4280
|
+
const range = selection.getRangeAt(0);
|
|
4281
|
+
const all = this.previewSelectAll;
|
|
4282
|
+
if (all && range.startContainer === all.start && range.startOffset === all.startOffset
|
|
4283
|
+
&& range.endContainer === all.end && range.endOffset === all.endOffset) {
|
|
4284
|
+
// Ctrl+A, Ctrl+C: the whole document as Formatted Text, like Copy All >
|
|
4285
|
+
// Formatted Text, so chunks not mounted yet are copied too. Nothing
|
|
4286
|
+
// goes on the clipboard synchronously: the mounted part alone would
|
|
4287
|
+
// be a silently partial copy.
|
|
4288
|
+
e.preventDefault();
|
|
4289
|
+
void this.copyAllWithImagesPreviewContent();
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4292
|
+
const { text, html } = this.previewSelectionContent(range);
|
|
4293
|
+
if (!html) {
|
|
4294
|
+
return;
|
|
4295
|
+
}
|
|
4296
|
+
e.preventDefault();
|
|
4297
|
+
e.clipboardData.setData('text/plain', text);
|
|
4298
|
+
e.clipboardData.setData('text/html', html);
|
|
4299
|
+
const copyId = (this.previewCopyId += 1);
|
|
4300
|
+
if (this.assetSession && /<img\b/i.test(html)) {
|
|
4301
|
+
void this.upgradePreviewCopyImages(copyId, html, text);
|
|
4302
|
+
}
|
|
4303
|
+
});
|
|
3978
4304
|
this.elPreviewPane.addEventListener('contextmenu', (e) => {
|
|
3979
4305
|
if (!this.controlPolicy.contextMenu.preview) {
|
|
3980
4306
|
return;
|
|
@@ -3983,11 +4309,12 @@ export class MdzipWorkspaceView {
|
|
|
3983
4309
|
// button collapses the browser selection before the action runs (same
|
|
3984
4310
|
// reason the editor menu captures its CodeMirror range up front).
|
|
3985
4311
|
const domSelection = this.elPreviewPane.ownerDocument.defaultView?.getSelection();
|
|
3986
|
-
const
|
|
3987
|
-
&& this.elPreviewContent.contains(domSelection.anchorNode)
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
4312
|
+
const selected = domSelection && !domSelection.isCollapsed
|
|
4313
|
+
&& this.elPreviewContent.contains(domSelection.anchorNode);
|
|
4314
|
+
const { text, html } = selected && domSelection.rangeCount > 0
|
|
4315
|
+
? this.previewSelectionContent(domSelection.getRangeAt(0))
|
|
4316
|
+
: { text: '', html: '' };
|
|
4317
|
+
this.contextMenuState = { kind: 'preview', text, html, x: e.clientX, y: e.clientY };
|
|
3991
4318
|
if (this.previewMenuItems().length === 0) {
|
|
3992
4319
|
this.contextMenuState = null;
|
|
3993
4320
|
return;
|
|
@@ -4119,7 +4446,7 @@ export class MdzipWorkspaceView {
|
|
|
4119
4446
|
async openPathAndScrollToAnchor(path, fragment) {
|
|
4120
4447
|
const generationBefore = this.previewGeneration;
|
|
4121
4448
|
this.pendingAnchorFragment = fragment || null;
|
|
4122
|
-
await this.
|
|
4449
|
+
await this.openPathOnly(path);
|
|
4123
4450
|
// No new render started (already on that document, or the open failed):
|
|
4124
4451
|
// nothing will call firePreviewRendered, so apply the anchor now.
|
|
4125
4452
|
if (this.pendingAnchorFragment !== null && this.previewGeneration === generationBefore) {
|
|
@@ -4292,6 +4619,10 @@ export class MdzipWorkspaceView {
|
|
|
4292
4619
|
if (collectMdzipHeadingIds(record.tokens).some((id) => id.toLowerCase() === lower)) {
|
|
4293
4620
|
return true;
|
|
4294
4621
|
}
|
|
4622
|
+
// The footnotes section (#58) renders footnote-<label> targets.
|
|
4623
|
+
if (lower.startsWith('footnote-') && record.tokens.some((token) => token.type === 'footnotes')) {
|
|
4624
|
+
return true;
|
|
4625
|
+
}
|
|
4295
4626
|
// An explicit <a id="x"> / <a name="x"> anchor written in the source.
|
|
4296
4627
|
const escaped = fragment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4297
4628
|
return new RegExp(`(?:id|name)\\s*=\\s*["']${escaped}["']`, 'i').test(record.key);
|
|
@@ -4361,9 +4692,53 @@ export class MdzipWorkspaceView {
|
|
|
4361
4692
|
this.anchorPin?.disconnect();
|
|
4362
4693
|
this.anchorPin = null;
|
|
4363
4694
|
}
|
|
4364
|
-
|
|
4695
|
+
/**
|
|
4696
|
+
* Opens another file of the current workspace, as clicking it in the file
|
|
4697
|
+
* tree does, optionally landing on a heading or anchor in it (#53). Paths
|
|
4698
|
+
* match the archive exactly (case-sensitive after NFC normalization).
|
|
4699
|
+
*
|
|
4700
|
+
* Resolves `true` once the file is shown and, when `anchor` is given, the
|
|
4701
|
+
* preview has scrolled to it — with the same pinning as {@link scrollToAnchor}
|
|
4702
|
+
* while images above it load. Resolves `false` for a path that isn't in the
|
|
4703
|
+
* workspace (the current file stays open) or an anchor that isn't found
|
|
4704
|
+
* (the file stays open at its top).
|
|
4705
|
+
*/
|
|
4706
|
+
async openPath(path, options = {}) {
|
|
4707
|
+
if (!(await this.openPathOnly(path))) {
|
|
4708
|
+
return false;
|
|
4709
|
+
}
|
|
4710
|
+
return options.anchor === undefined ? true : this.scrollToAnchor(options.anchor);
|
|
4711
|
+
}
|
|
4712
|
+
/**
|
|
4713
|
+
* Changes the file name shown in the title bar and used to save, without
|
|
4714
|
+
* reopening the workspace or changing the selected file, e.g. after Save As
|
|
4715
|
+
* (#51). Returns false, changing nothing, when no workspace is open or the
|
|
4716
|
+
* new name implies a different format (`.md` vs `.mdz`): that needs a reopen.
|
|
4717
|
+
*/
|
|
4718
|
+
setFileName(fileName) {
|
|
4719
|
+
const snapshot = this.workspace?.snapshot();
|
|
4720
|
+
if (!this.workspace || !snapshot) {
|
|
4721
|
+
return false;
|
|
4722
|
+
}
|
|
4723
|
+
const lower = fileName.toLowerCase();
|
|
4724
|
+
const impliedFormat = /\.(?:md|markdown|mdown|mkd)$/.test(lower)
|
|
4725
|
+
? 'markdown'
|
|
4726
|
+
: lower.endsWith('.mdz') ? 'mdz' : snapshot.sourceFormat;
|
|
4727
|
+
if (impliedFormat !== snapshot.sourceFormat) {
|
|
4728
|
+
return false;
|
|
4729
|
+
}
|
|
4730
|
+
if (this.workspace.setFileName(fileName)) {
|
|
4731
|
+
this.render();
|
|
4732
|
+
}
|
|
4733
|
+
return true;
|
|
4734
|
+
}
|
|
4735
|
+
/** The archive path of the file currently shown, or null before a workspace opens. */
|
|
4736
|
+
getCurrentPath() {
|
|
4737
|
+
return this.workspace?.snapshot().currentPath ?? null;
|
|
4738
|
+
}
|
|
4739
|
+
async openPathOnly(path) {
|
|
4365
4740
|
if (!this.workspace) {
|
|
4366
|
-
return;
|
|
4741
|
+
return false;
|
|
4367
4742
|
}
|
|
4368
4743
|
try {
|
|
4369
4744
|
const opened = await this.workspace.openPath(path);
|
|
@@ -4371,9 +4746,11 @@ export class MdzipWorkspaceView {
|
|
|
4371
4746
|
this.layout = this.validLayoutForSnapshot(this.layout, this.workspace.snapshot());
|
|
4372
4747
|
this.render();
|
|
4373
4748
|
}
|
|
4749
|
+
return opened;
|
|
4374
4750
|
}
|
|
4375
4751
|
catch (error) {
|
|
4376
4752
|
this.options.onFailed?.(error);
|
|
4753
|
+
return false;
|
|
4377
4754
|
}
|
|
4378
4755
|
}
|
|
4379
4756
|
handleTooltipPointer(event) {
|
|
@@ -4405,9 +4782,17 @@ export class MdzipWorkspaceView {
|
|
|
4405
4782
|
}
|
|
4406
4783
|
showTooltipForElementNow(element) {
|
|
4407
4784
|
const rect = element.getBoundingClientRect();
|
|
4408
|
-
|
|
4785
|
+
// A menu item's tip goes beside it, so it doesn't cover the items above:
|
|
4786
|
+
// context menus and the toolbar's dropdowns (color scheme, page width).
|
|
4787
|
+
const placement = element.closest('.nav-context-menu, .theme-popover, .toolbar-popover') ? 'side' : 'above';
|
|
4788
|
+
this.showTooltip(element.dataset['tooltip'] ?? '', rect.left + rect.width / 2, rect.top, placement, rect);
|
|
4409
4789
|
}
|
|
4410
|
-
|
|
4790
|
+
/**
|
|
4791
|
+
* Shows the tooltip centered above (anchorX, anchorY), or, with `side`
|
|
4792
|
+
* placement, beside `anchorRect` (to its right, or its left when there's no
|
|
4793
|
+
* room), vertically centered on it.
|
|
4794
|
+
*/
|
|
4795
|
+
showTooltip(text, anchorX, anchorY, placement = 'above', anchorRect) {
|
|
4411
4796
|
if (!text) {
|
|
4412
4797
|
this.hideTooltip();
|
|
4413
4798
|
return;
|
|
@@ -4420,9 +4805,23 @@ export class MdzipWorkspaceView {
|
|
|
4420
4805
|
const offset = 8;
|
|
4421
4806
|
this.elTooltip.textContent = text;
|
|
4422
4807
|
this.elTooltip.hidden = false;
|
|
4808
|
+
// Measured at the origin, so its previous position doesn't make it wrap early.
|
|
4809
|
+
this.elTooltip.style.left = '0px';
|
|
4810
|
+
this.elTooltip.style.top = '0px';
|
|
4423
4811
|
const rect = this.elTooltip.getBoundingClientRect();
|
|
4424
|
-
|
|
4425
|
-
|
|
4812
|
+
let x;
|
|
4813
|
+
let y;
|
|
4814
|
+
if (placement === 'side' && anchorRect) {
|
|
4815
|
+
const right = anchorRect.right + offset;
|
|
4816
|
+
x = right + rect.width <= window.innerWidth - viewportPadding
|
|
4817
|
+
? right
|
|
4818
|
+
: Math.max(viewportPadding, anchorRect.left - offset - rect.width);
|
|
4819
|
+
y = Math.max(viewportPadding, Math.min(anchorRect.top + (anchorRect.height - rect.height) / 2, window.innerHeight - rect.height - viewportPadding));
|
|
4820
|
+
}
|
|
4821
|
+
else {
|
|
4822
|
+
x = Math.max(viewportPadding, Math.min(anchorX - rect.width / 2, window.innerWidth - rect.width - viewportPadding));
|
|
4823
|
+
y = Math.max(viewportPadding, anchorY - rect.height - offset);
|
|
4824
|
+
}
|
|
4426
4825
|
this.tooltipState = { text, x, y };
|
|
4427
4826
|
this.elTooltip.style.left = `${Math.round(x)}px`;
|
|
4428
4827
|
this.elTooltip.style.top = `${Math.round(y)}px`;
|
|
@@ -5224,21 +5623,43 @@ export class MdzipWorkspaceView {
|
|
|
5224
5623
|
return [];
|
|
5225
5624
|
}
|
|
5226
5625
|
const items = [];
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
label: '
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5626
|
+
const markdown = this.workspace?.snapshot().currentPathType === 'markdown';
|
|
5627
|
+
// Copy: the selection, in the same formats as Copy All below (no
|
|
5628
|
+
// Markdown: see there). A selection of only an image has no text, but
|
|
5629
|
+
// still has markup.
|
|
5630
|
+
const copy = [];
|
|
5631
|
+
if (state.text.trim()) {
|
|
5632
|
+
copy.push({ action: 'preview-copy', label: 'Plain Text', tooltip: PLAIN_TEXT_TOOLTIP });
|
|
5633
|
+
}
|
|
5634
|
+
if (markdown && state.html) {
|
|
5635
|
+
copy.push({
|
|
5636
|
+
action: 'preview-copy-selection-formatted',
|
|
5637
|
+
label: 'Formatted Text',
|
|
5638
|
+
shortcut: this.editorShortcut('C'),
|
|
5639
|
+
tooltip: FORMATTED_TEXT_TOOLTIP
|
|
5640
|
+
}, { action: 'preview-copy-selection-html', label: 'HTML', tooltip: "The selection's HTML markup, as text" });
|
|
5641
|
+
}
|
|
5642
|
+
if (copy.length > 0) {
|
|
5643
|
+
items.push({ action: 'preview-copy-menu', label: 'Copy', icon: MENU_COPY_ICON_HTML, submenu: copy }, null);
|
|
5644
|
+
}
|
|
5645
|
+
// Copy All: the whole document, in the format each item names. Markdown
|
|
5646
|
+
// and HTML are source copies (#56); the preview can't map a selection
|
|
5647
|
+
// back to exact Markdown (the source pane's own Copy does that).
|
|
5648
|
+
const copyAll = [
|
|
5649
|
+
{ action: 'preview-copy-all', label: 'Plain Text', tooltip: PLAIN_TEXT_TOOLTIP },
|
|
5650
|
+
{
|
|
5651
|
+
action: 'preview-copy-all-images',
|
|
5652
|
+
label: 'Formatted Text',
|
|
5653
|
+
shortcut: `${this.editorShortcut('A')}, ${this.editorShortcut('C')}`,
|
|
5654
|
+
tooltip: FORMATTED_TEXT_TOOLTIP
|
|
5655
|
+
}
|
|
5656
|
+
];
|
|
5657
|
+
if (markdown) {
|
|
5658
|
+
copyAll.push({ action: 'preview-copy-markdown', label: 'Markdown', tooltip: "The document's Markdown source" }, { action: 'preview-copy-html', label: 'HTML', tooltip: "The document's HTML markup, as text" });
|
|
5659
|
+
}
|
|
5660
|
+
items.push({ action: 'preview-copy-all-menu', label: 'Copy All', icon: MENU_COPY_ICON_HTML, submenu: copyAll });
|
|
5661
|
+
// Same item and group as the source editor's menu.
|
|
5662
|
+
items.push(null, { action: 'preview-select-all', label: 'Select All', icon: MENU_SELECT_ALL_ICON_HTML, shortcut: this.editorShortcut('A') });
|
|
5242
5663
|
return items;
|
|
5243
5664
|
}
|
|
5244
5665
|
handlePreviewMenuAction(action) {
|
|
@@ -5255,6 +5676,27 @@ export class MdzipWorkspaceView {
|
|
|
5255
5676
|
case 'preview-copy-all-images':
|
|
5256
5677
|
void this.copyAllWithImagesPreviewContent();
|
|
5257
5678
|
break;
|
|
5679
|
+
case 'preview-copy-html':
|
|
5680
|
+
void this.copyHtmlPreviewContent();
|
|
5681
|
+
break;
|
|
5682
|
+
case 'preview-copy-markdown': {
|
|
5683
|
+
const markdown = this.workspace?.snapshot().currentText ?? '';
|
|
5684
|
+
void this.writeClipboardText(markdown, 'Markdown copied to clipboard')
|
|
5685
|
+
.then((outcome) => this.finishCopyNotification(false, outcome));
|
|
5686
|
+
break;
|
|
5687
|
+
}
|
|
5688
|
+
case 'preview-select-all':
|
|
5689
|
+
this.elPreviewPane.focus({ preventScroll: true });
|
|
5690
|
+
this.selectAllPreview();
|
|
5691
|
+
break;
|
|
5692
|
+
case 'preview-copy-selection-formatted':
|
|
5693
|
+
void this.copyPreviewSelectionFormatted(state.html, state.text)
|
|
5694
|
+
.then((outcome) => this.finishCopyNotification(false, outcome));
|
|
5695
|
+
break;
|
|
5696
|
+
case 'preview-copy-selection-html':
|
|
5697
|
+
void this.writeClipboardText(state.html, 'HTML copied to clipboard as text')
|
|
5698
|
+
.then((outcome) => this.finishCopyNotification(false, outcome));
|
|
5699
|
+
break;
|
|
5258
5700
|
case 'preview-copy':
|
|
5259
5701
|
// Partial Copy never shows the render dialog (there's nothing to
|
|
5260
5702
|
// drain), so its confirmation is always the brief toast.
|
|
@@ -5333,19 +5775,223 @@ export class MdzipWorkspaceView {
|
|
|
5333
5775
|
}
|
|
5334
5776
|
}
|
|
5335
5777
|
/** Writes `text` to the clipboard. Returns null if there was nothing to copy (a true no-op, not a failure); otherwise a success message or the failure text (also reported via `onFailed`, but that's a host callback with no guaranteed visible surface — the caller needs the text itself to show the user). */
|
|
5336
|
-
|
|
5778
|
+
copyPreviewSelection(text) {
|
|
5779
|
+
return this.writeClipboardText(text, 'Plain text copied to clipboard');
|
|
5780
|
+
}
|
|
5781
|
+
/**
|
|
5782
|
+
* A preview selection as the Copy items copy it. `html`: the selected
|
|
5783
|
+
* markup inside its enclosing elements (so part of a list item is still
|
|
5784
|
+
* `<ul><li>…`), without the editor's own chrome (code-block headers, image
|
|
5785
|
+
* slots and edit buttons, chunk wrappers) and with archive images' `src`
|
|
5786
|
+
* back to their archive paths instead of blob: URLs. `text`: its plain
|
|
5787
|
+
* text, cleaned the same way as Copy All's (see previewPlainText).
|
|
5788
|
+
*/
|
|
5789
|
+
previewSelectionContent(range) {
|
|
5790
|
+
const doc = this.elPreviewContent.ownerDocument;
|
|
5791
|
+
let content = range.cloneContents();
|
|
5792
|
+
let ancestor = range.commonAncestorContainer;
|
|
5793
|
+
if (ancestor.nodeType !== 1) {
|
|
5794
|
+
ancestor = ancestor.parentNode;
|
|
5795
|
+
}
|
|
5796
|
+
while (ancestor && ancestor.nodeType === 1 && ancestor !== this.elPreviewContent
|
|
5797
|
+
&& this.elPreviewContent.contains(ancestor)) {
|
|
5798
|
+
const wrapper = ancestor.cloneNode(false);
|
|
5799
|
+
wrapper.appendChild(content);
|
|
5800
|
+
content = wrapper;
|
|
5801
|
+
ancestor = ancestor.parentNode;
|
|
5802
|
+
}
|
|
5803
|
+
const container = doc.createElement('div');
|
|
5804
|
+
container.appendChild(content);
|
|
5805
|
+
const text = previewPlainText(container);
|
|
5806
|
+
for (const element of Array.from(container.querySelectorAll(PREVIEW_CHROME_REMOVE_SELECTOR))) {
|
|
5807
|
+
element.remove();
|
|
5808
|
+
}
|
|
5809
|
+
for (const element of Array.from(container.querySelectorAll(PREVIEW_CHROME_UNWRAP_SELECTOR))) {
|
|
5810
|
+
element.replaceWith(...Array.from(element.childNodes));
|
|
5811
|
+
}
|
|
5812
|
+
for (const image of Array.from(container.querySelectorAll('img'))) {
|
|
5813
|
+
const source = image.getAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE);
|
|
5814
|
+
if (source !== null) {
|
|
5815
|
+
image.setAttribute('src', source);
|
|
5816
|
+
image.removeAttribute(PREVIEW_IMAGE_SOURCE_ATTRIBUTE);
|
|
5817
|
+
}
|
|
5818
|
+
for (const name of Array.from(image.classList)) {
|
|
5819
|
+
if (name === 'mdzip-image-loading') {
|
|
5820
|
+
image.classList.remove(name);
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5823
|
+
if (!image.classList.length) {
|
|
5824
|
+
image.removeAttribute('class');
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
return { text, html: container.innerHTML.trim() };
|
|
5828
|
+
}
|
|
5829
|
+
/**
|
|
5830
|
+
* Selects all of the rendered document (Ctrl+A and the menu's Select All),
|
|
5831
|
+
* remembering the selection so Ctrl+C copies the whole document while it's
|
|
5832
|
+
* unchanged (see the copy handler).
|
|
5833
|
+
*/
|
|
5834
|
+
selectAllPreview() {
|
|
5835
|
+
const doc = this.elPreviewContent.ownerDocument;
|
|
5836
|
+
const selection = doc.defaultView?.getSelection();
|
|
5837
|
+
if (!selection) {
|
|
5838
|
+
return;
|
|
5839
|
+
}
|
|
5840
|
+
const range = doc.createRange();
|
|
5841
|
+
range.selectNodeContents(this.elPreviewContent);
|
|
5842
|
+
selection.removeAllRanges();
|
|
5843
|
+
selection.addRange(range);
|
|
5844
|
+
this.previewSelectAll = {
|
|
5845
|
+
start: range.startContainer, startOffset: range.startOffset,
|
|
5846
|
+
end: range.endContainer, endOffset: range.endOffset
|
|
5847
|
+
};
|
|
5848
|
+
}
|
|
5849
|
+
/**
|
|
5850
|
+
* Some hosts run their own Select All after the key event, even when the
|
|
5851
|
+
* editor handled it: a VS Code webview selects the whole page, source pane
|
|
5852
|
+
* included. For a moment after Ctrl+A, a selection that reaches outside
|
|
5853
|
+
* the preview is put back to the preview's own.
|
|
5854
|
+
*/
|
|
5855
|
+
keepPreviewSelectAll() {
|
|
5856
|
+
const doc = this.elPreviewContent.ownerDocument;
|
|
5857
|
+
const stop = () => doc.removeEventListener('selectionchange', onChange);
|
|
5858
|
+
const onChange = () => {
|
|
5859
|
+
const selection = doc.defaultView?.getSelection();
|
|
5860
|
+
if (this.destroyed || !this.previewSelectAll || !selection || selection.rangeCount === 0) {
|
|
5861
|
+
stop();
|
|
5862
|
+
return;
|
|
5863
|
+
}
|
|
5864
|
+
const range = selection.getRangeAt(0);
|
|
5865
|
+
if (range.intersectsNode(this.elPreviewContent) && !this.elPreviewContent.contains(range.commonAncestorContainer)
|
|
5866
|
+
&& range.commonAncestorContainer !== this.elPreviewContent) {
|
|
5867
|
+
stop();
|
|
5868
|
+
this.selectAllPreview();
|
|
5869
|
+
}
|
|
5870
|
+
};
|
|
5871
|
+
doc.addEventListener('selectionchange', onChange);
|
|
5872
|
+
setTimeout(stop, 500);
|
|
5873
|
+
}
|
|
5874
|
+
/**
|
|
5875
|
+
* Re-copies a Ctrl+C selection with its images embedded. Best effort: the
|
|
5876
|
+
* synchronous copy already on the clipboard stays if this fails, and it's
|
|
5877
|
+
* skipped if another copy happened meanwhile.
|
|
5878
|
+
*/
|
|
5879
|
+
async upgradePreviewCopyImages(copyId, html, text) {
|
|
5880
|
+
const snapshot = this.workspace?.snapshot();
|
|
5881
|
+
const clipboard = this.editorClipboard();
|
|
5882
|
+
const ClipboardItemCtor = this.elPreviewContent.ownerDocument.defaultView?.ClipboardItem;
|
|
5883
|
+
if (!this.assetSession || !snapshot || !clipboard || !ClipboardItemCtor || !('write' in clipboard)) {
|
|
5884
|
+
return;
|
|
5885
|
+
}
|
|
5886
|
+
try {
|
|
5887
|
+
const embedded = await this.assetSession.rewriteHtmlEmbeddingImages(html, snapshot.currentPath, new AbortController().signal);
|
|
5888
|
+
if (copyId !== this.previewCopyId || this.destroyed) {
|
|
5889
|
+
return;
|
|
5890
|
+
}
|
|
5891
|
+
await clipboard.write([new ClipboardItemCtor({
|
|
5892
|
+
'text/html': new Blob([embedded], { type: 'text/html' }),
|
|
5893
|
+
'text/plain': new Blob([text], { type: 'text/plain' })
|
|
5894
|
+
})]);
|
|
5895
|
+
}
|
|
5896
|
+
catch {
|
|
5897
|
+
// The plain copy is already on the clipboard.
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
/**
|
|
5901
|
+
* Copies a preview selection as formatted text (Copy > Formatted Text, and
|
|
5902
|
+
* Ctrl+C's image upgrade): its HTML with archive images embedded as data:
|
|
5903
|
+
* URLs, so they survive pasting into Word or email, plus its plain text.
|
|
5904
|
+
*/
|
|
5905
|
+
async copyPreviewSelectionFormatted(html, text) {
|
|
5906
|
+
const snapshot = this.workspace?.snapshot();
|
|
5907
|
+
let imageCount = 0;
|
|
5908
|
+
let embedded = html;
|
|
5909
|
+
if (this.assetSession && snapshot && /<img\b/i.test(html)) {
|
|
5910
|
+
try {
|
|
5911
|
+
embedded = await this.assetSession.rewriteHtmlEmbeddingImages(html, snapshot.currentPath, new AbortController().signal, (_done, total) => { imageCount = total; });
|
|
5912
|
+
}
|
|
5913
|
+
catch (error) {
|
|
5914
|
+
this.options.onFailed?.(error);
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
return this.writeRichClipboard(embedded, text, imageCount);
|
|
5918
|
+
}
|
|
5919
|
+
/** `writeText` with the same timeout and outcome reporting as {@link copyPreviewSelection}; `message` is the success confirmation. */
|
|
5920
|
+
async writeClipboardText(text, message) {
|
|
5337
5921
|
if (!text) {
|
|
5338
5922
|
return null;
|
|
5339
5923
|
}
|
|
5340
5924
|
try {
|
|
5341
5925
|
await this.withTimeout(Promise.resolve(this.editorClipboard()?.writeText(text)), this.clipboardFallbackWriteTimeoutMs, 'Clipboard write timed out.');
|
|
5342
|
-
return { message
|
|
5926
|
+
return { message };
|
|
5343
5927
|
}
|
|
5344
5928
|
catch (error) {
|
|
5345
5929
|
this.options.onFailed?.(error);
|
|
5346
5930
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
5347
5931
|
}
|
|
5348
5932
|
}
|
|
5933
|
+
/**
|
|
5934
|
+
* Copies the whole document's rendered HTML markup as plain text (#56), so
|
|
5935
|
+
* any paste target, Notepad included, gets the raw `<h1>…</h1><p>…</p>`.
|
|
5936
|
+
* Rendered fresh like Copy All with Images, not read from the live preview
|
|
5937
|
+
* (which holds hydrated `blob:` image URLs, lazy-mount sentinels and
|
|
5938
|
+
* code-block chrome): images keep their archive-relative `src`, and
|
|
5939
|
+
* extension `mount()` output (e.g. mermaid diagrams) copies as its
|
|
5940
|
+
* pre-render markup. Large documents get the same cancelable progress
|
|
5941
|
+
* dialog and "ready to copy" step. A host's custom (non-chunking) renderer
|
|
5942
|
+
* renders the document in one pass instead.
|
|
5943
|
+
*/
|
|
5944
|
+
async copyHtmlPreviewContent() {
|
|
5945
|
+
const snapshot = this.workspace?.snapshot();
|
|
5946
|
+
if (!snapshot || snapshot.currentPathType !== 'markdown') {
|
|
5947
|
+
return;
|
|
5948
|
+
}
|
|
5949
|
+
const message = 'HTML copied to clipboard as text';
|
|
5950
|
+
const abort = new AbortController();
|
|
5951
|
+
let dialogShown = false;
|
|
5952
|
+
const showDialogTimer = setTimeout(() => {
|
|
5953
|
+
dialogShown = true;
|
|
5954
|
+
this.copyRenderDialogState = { done: 0, total: 0, label: 'Rendering the document', abort };
|
|
5955
|
+
this.render();
|
|
5956
|
+
}, 200);
|
|
5957
|
+
let html;
|
|
5958
|
+
try {
|
|
5959
|
+
const context = this.createMarkdownContext(snapshot, abort.signal);
|
|
5960
|
+
if (this.renderingService.supportsChunking) {
|
|
5961
|
+
const cacheGeneration = this.previewMemoMatchesSnapshot(snapshot) ? this.previewGeneration : null;
|
|
5962
|
+
html = await this.renderFullDocumentHtml(snapshot, context, abort.signal, cacheGeneration, (done, total) => {
|
|
5963
|
+
if (!dialogShown)
|
|
5964
|
+
return;
|
|
5965
|
+
this.copyRenderDialogState = { done, total, label: 'Rendering the document', abort };
|
|
5966
|
+
this.updateCopyRenderDialogProgress();
|
|
5967
|
+
});
|
|
5968
|
+
}
|
|
5969
|
+
else {
|
|
5970
|
+
html = await this.renderingService.renderMarkdown(snapshot.currentText, context);
|
|
5971
|
+
}
|
|
5972
|
+
}
|
|
5973
|
+
catch (error) {
|
|
5974
|
+
clearTimeout(showDialogTimer);
|
|
5975
|
+
if (error?.name === 'AbortError') {
|
|
5976
|
+
if (dialogShown) {
|
|
5977
|
+
this.copyRenderDialogState = null;
|
|
5978
|
+
this.render();
|
|
5979
|
+
}
|
|
5980
|
+
}
|
|
5981
|
+
else {
|
|
5982
|
+
this.finishCopyNotification(dialogShown, { error: error instanceof Error ? error.message : String(error) });
|
|
5983
|
+
this.options.onFailed?.(error);
|
|
5984
|
+
}
|
|
5985
|
+
return;
|
|
5986
|
+
}
|
|
5987
|
+
clearTimeout(showDialogTimer);
|
|
5988
|
+
if (dialogShown) {
|
|
5989
|
+
this.armCopyReady(() => this.writeClipboardText(html, message));
|
|
5990
|
+
return;
|
|
5991
|
+
}
|
|
5992
|
+
const outcome = await this.writeClipboardText(html, message);
|
|
5993
|
+
this.finishCopyNotification(false, outcome);
|
|
5994
|
+
}
|
|
5349
5995
|
/** Syncs just the copy-render dialog's progress bar/text from `copyRenderDialogState` — see `copyAllPreviewContent` for why this bypasses `render()`. No-op while the dialog isn't showing. */
|
|
5350
5996
|
updateCopyRenderDialogProgress() {
|
|
5351
5997
|
if (!this.copyRenderDialogState) {
|
|
@@ -5371,7 +6017,7 @@ export class MdzipWorkspaceView {
|
|
|
5371
6017
|
async copyAllPreviewContent() {
|
|
5372
6018
|
const pending = this.chunkedRenderState;
|
|
5373
6019
|
if (!pending || pending.cursor >= pending.records.length) {
|
|
5374
|
-
const outcome = await this.copyPreviewSelection(this.elPreviewContent
|
|
6020
|
+
const outcome = await this.copyPreviewSelection(previewPlainText(this.elPreviewContent));
|
|
5375
6021
|
this.finishCopyNotification(false, outcome);
|
|
5376
6022
|
return;
|
|
5377
6023
|
}
|
|
@@ -5417,10 +6063,10 @@ export class MdzipWorkspaceView {
|
|
|
5417
6063
|
// activation by now, so the actual write waits for a fresh one
|
|
5418
6064
|
// (the dialog's own Copy button) instead of firing automatically.
|
|
5419
6065
|
// See copyReadyState's doc comment.
|
|
5420
|
-
this.armCopyReady(() => this.copyPreviewSelection(this.elPreviewContent
|
|
6066
|
+
this.armCopyReady(() => this.copyPreviewSelection(previewPlainText(this.elPreviewContent)));
|
|
5421
6067
|
return;
|
|
5422
6068
|
}
|
|
5423
|
-
const outcome = await this.copyPreviewSelection(this.elPreviewContent
|
|
6069
|
+
const outcome = await this.copyPreviewSelection(previewPlainText(this.elPreviewContent));
|
|
5424
6070
|
this.finishCopyNotification(false, outcome);
|
|
5425
6071
|
}
|
|
5426
6072
|
/**
|
|
@@ -5579,7 +6225,7 @@ export class MdzipWorkspaceView {
|
|
|
5579
6225
|
clearTimeout(showDialogTimer);
|
|
5580
6226
|
const scratch = this.elPreviewContent.ownerDocument.createElement('div');
|
|
5581
6227
|
scratch.innerHTML = finalHtml;
|
|
5582
|
-
const plainText = scratch
|
|
6228
|
+
const plainText = previewPlainText(scratch);
|
|
5583
6229
|
if (dialogShown) {
|
|
5584
6230
|
// Same reasoning as copyAllPreviewContent: the prepare phase (often
|
|
5585
6231
|
// the slower of the two here, once image embedding is involved) has
|
|
@@ -5623,8 +6269,8 @@ export class MdzipWorkspaceView {
|
|
|
5623
6269
|
await this.withTimeout(clipboard.write([item]), this.clipboardWriteTimeoutMs, 'Clipboard write timed out.');
|
|
5624
6270
|
return {
|
|
5625
6271
|
message: imageCount > 0
|
|
5626
|
-
? `
|
|
5627
|
-
: '
|
|
6272
|
+
? `Formatted text with ${imageCount} image${imageCount === 1 ? '' : 's'} copied to clipboard`
|
|
6273
|
+
: 'Formatted text copied to clipboard'
|
|
5628
6274
|
};
|
|
5629
6275
|
}
|
|
5630
6276
|
catch (error) {
|
|
@@ -6123,22 +6769,138 @@ export class MdzipWorkspaceView {
|
|
|
6123
6769
|
* reacting to `data-vscode-theme-kind` changes via MutationObserver — so the
|
|
6124
6770
|
* editor follows the host theme without being recreated (recreation would
|
|
6125
6771
|
* destroy the CodeMirror instance and lose unsaved edits). No-op when the
|
|
6126
|
-
* scheme is unchanged.
|
|
6127
|
-
*
|
|
6772
|
+
* scheme is unchanged. `'system'` follows the operating system's setting
|
|
6773
|
+
* from then on. Does not fire `onColorSchemeChanged` or
|
|
6774
|
+
* `onColorSchemePreferenceChanged`, which only report choices made in the
|
|
6775
|
+
* built-in toolbar menu.
|
|
6128
6776
|
*/
|
|
6129
6777
|
setColorScheme(colorScheme) {
|
|
6130
|
-
|
|
6778
|
+
const next = this.resolveColorScheme(colorScheme);
|
|
6779
|
+
if (this.colorSchemePreference === colorScheme && this.colorScheme === next) {
|
|
6131
6780
|
return;
|
|
6132
6781
|
}
|
|
6133
|
-
this.
|
|
6782
|
+
this.colorSchemePreference = colorScheme;
|
|
6783
|
+
this.colorScheme = next;
|
|
6134
6784
|
this.render();
|
|
6135
6785
|
}
|
|
6136
|
-
|
|
6137
|
-
|
|
6786
|
+
/**
|
|
6787
|
+
* Sets what the menu's System choice follows: the host theme in `source`
|
|
6788
|
+
* (its current scheme, and optionally the choice's name and icon), or the
|
|
6789
|
+
* operating system's setting again with `null`. Call it whenever the host
|
|
6790
|
+
* theme changes; while System is chosen the preview switches with it,
|
|
6791
|
+
* keeping the rendered preview in place. Like `setColorScheme`, it fires
|
|
6792
|
+
* no change callbacks.
|
|
6793
|
+
*/
|
|
6794
|
+
setSystemColorScheme(source) {
|
|
6795
|
+
this.systemColorSource = source;
|
|
6796
|
+
this.colorScheme = this.resolveColorScheme(this.colorSchemePreference);
|
|
6797
|
+
this.render();
|
|
6798
|
+
}
|
|
6799
|
+
/** The light/dark scheme a choice means right now; `'system'` reads the host theme or the OS setting. */
|
|
6800
|
+
resolveColorScheme(preference) {
|
|
6801
|
+
if (preference !== 'system') {
|
|
6802
|
+
return preference;
|
|
6803
|
+
}
|
|
6804
|
+
if (this.systemColorSource) {
|
|
6805
|
+
return this.systemColorSource.colorScheme;
|
|
6806
|
+
}
|
|
6807
|
+
return this.systemColorSchemeQuery?.matches ? 'dark' : 'light';
|
|
6808
|
+
}
|
|
6809
|
+
themeMenuItems() {
|
|
6810
|
+
return [
|
|
6811
|
+
[this.elLightThemeBtn, 'light'],
|
|
6812
|
+
[this.elDarkThemeBtn, 'dark'],
|
|
6813
|
+
[this.elSystemThemeBtn, 'system']
|
|
6814
|
+
];
|
|
6815
|
+
}
|
|
6816
|
+
/** Arrow keys cycle focus through a toolbar menu's items. */
|
|
6817
|
+
moveMenuFocus(e, buttons) {
|
|
6818
|
+
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') {
|
|
6138
6819
|
return;
|
|
6139
6820
|
}
|
|
6140
|
-
|
|
6141
|
-
this.
|
|
6821
|
+
e.preventDefault();
|
|
6822
|
+
const current = buttons.indexOf(this.elRoot.ownerDocument.activeElement);
|
|
6823
|
+
const step = e.key === 'ArrowDown' ? 1 : -1;
|
|
6824
|
+
buttons[(current + step + buttons.length) % buttons.length].focus();
|
|
6825
|
+
}
|
|
6826
|
+
widthMenuItems() {
|
|
6827
|
+
return Array.from(this.elWidthPopover.querySelectorAll('[data-width]'))
|
|
6828
|
+
.map((button) => [button, button.dataset['width']]);
|
|
6829
|
+
}
|
|
6830
|
+
/** The menu choice matching the current width, or null for a custom pixel width. */
|
|
6831
|
+
previewWidthChoice() {
|
|
6832
|
+
if (this.previewMaxWidth === undefined) {
|
|
6833
|
+
return 'default';
|
|
6834
|
+
}
|
|
6835
|
+
return typeof this.previewMaxWidth === 'number' ? null : this.previewMaxWidth;
|
|
6836
|
+
}
|
|
6837
|
+
/**
|
|
6838
|
+
* A choice's label: its own width (base width times zoom) and, when `measure`
|
|
6839
|
+
* is set and the preview pane is narrower than that, the width it fits to.
|
|
6840
|
+
* Full Width has no number: it is always the pane's width.
|
|
6841
|
+
*/
|
|
6842
|
+
previewWidthLabel(choice, measure) {
|
|
6843
|
+
if (choice === 'full') {
|
|
6844
|
+
return PREVIEW_WIDTH_LABELS[choice];
|
|
6845
|
+
}
|
|
6846
|
+
const width = Math.round(PREVIEW_MAX_WIDTH_ALIASES[choice] * this.zoom);
|
|
6847
|
+
const pane = measure ? this.elPreviewPane.clientWidth : 0;
|
|
6848
|
+
const fit = pane > 0 && pane < width ? `, fits to ${pane} px` : '';
|
|
6849
|
+
return `${PREVIEW_WIDTH_LABELS[choice]} (${width} px${fit})`;
|
|
6850
|
+
}
|
|
6851
|
+
/** Labels each width menu item, and the button, with the pixel width it gives. */
|
|
6852
|
+
updateWidthLabels(measure) {
|
|
6853
|
+
for (const [button, choice] of this.widthMenuItems()) {
|
|
6854
|
+
const label = this.previewWidthLabel(choice, measure);
|
|
6855
|
+
setTooltip(button, label);
|
|
6856
|
+
button.setAttribute('aria-label', label);
|
|
6857
|
+
}
|
|
6858
|
+
const choice = this.previewWidthChoice();
|
|
6859
|
+
const label = `Page width: ${choice ? this.previewWidthLabel(choice, measure) : `${this.previewMaxWidth} px`}`;
|
|
6860
|
+
setTooltip(this.elWidthMenuBtn, label);
|
|
6861
|
+
this.elWidthMenuBtn.setAttribute('aria-label', label);
|
|
6862
|
+
}
|
|
6863
|
+
choosePreviewWidthFromMenu(choice) {
|
|
6864
|
+
const changed = this.previewWidthChoice() !== choice;
|
|
6865
|
+
this.previewMaxWidth = choice;
|
|
6866
|
+
this.closeWidthMenu(true);
|
|
6867
|
+
if (changed) {
|
|
6868
|
+
this.options.onPreviewWidthChanged?.(choice);
|
|
6869
|
+
}
|
|
6870
|
+
}
|
|
6871
|
+
/** Closes the page-width menu; `refocus` returns focus to its button (keyboard use). */
|
|
6872
|
+
closeWidthMenu(refocus) {
|
|
6873
|
+
this.widthMenuOpen = false;
|
|
6874
|
+
this.render();
|
|
6875
|
+
if (refocus) {
|
|
6876
|
+
this.elWidthMenuBtn.focus();
|
|
6877
|
+
}
|
|
6878
|
+
}
|
|
6879
|
+
chooseColorSchemeFromMenu(preference) {
|
|
6880
|
+
this.setColorSchemeFromToolbar(preference);
|
|
6881
|
+
this.closeThemeMenu(true);
|
|
6882
|
+
}
|
|
6883
|
+
/** Closes the color scheme menu; `refocus` returns focus to its button (keyboard use). */
|
|
6884
|
+
closeThemeMenu(refocus) {
|
|
6885
|
+
if (!this.themeMenuOpen) {
|
|
6886
|
+
return;
|
|
6887
|
+
}
|
|
6888
|
+
this.themeMenuOpen = false;
|
|
6889
|
+
this.render();
|
|
6890
|
+
if (refocus) {
|
|
6891
|
+
this.elThemeMenuBtn.focus();
|
|
6892
|
+
}
|
|
6893
|
+
}
|
|
6894
|
+
setColorSchemeFromToolbar(preference) {
|
|
6895
|
+
if (this.colorSchemePreference === preference) {
|
|
6896
|
+
return;
|
|
6897
|
+
}
|
|
6898
|
+
const before = this.colorScheme;
|
|
6899
|
+
this.setColorScheme(preference);
|
|
6900
|
+
this.options.onColorSchemePreferenceChanged?.(preference);
|
|
6901
|
+
if (this.colorScheme !== before) {
|
|
6902
|
+
this.options.onColorSchemeChanged?.(this.colorScheme);
|
|
6903
|
+
}
|
|
6142
6904
|
}
|
|
6143
6905
|
async setLayout(requested) {
|
|
6144
6906
|
const snapshot = this.workspace?.snapshot();
|
|
@@ -7002,325 +7764,348 @@ function formatMetadataValue(value) {
|
|
|
7002
7764
|
}
|
|
7003
7765
|
return 'Not available';
|
|
7004
7766
|
}
|
|
7005
|
-
const SHELL_HTML = `
|
|
7006
|
-
<section class="mdzip-root">
|
|
7007
|
-
<div class="document-strip" data-ref="document-strip" hidden>
|
|
7008
|
-
<button type="button" class="title-button" data-ref="title-btn"></button>
|
|
7009
|
-
<button type="button" class="document-info-button" data-ref="document-info-btn"
|
|
7010
|
-
title="Document information" aria-label="Document information">
|
|
7011
|
-
${INFO_ICON_HTML}
|
|
7012
|
-
</button>
|
|
7013
|
-
</div>
|
|
7014
|
-
|
|
7015
|
-
<header class="toolbar" data-ref="toolbar" hidden>
|
|
7016
|
-
<div class="toolbar-start">
|
|
7017
|
-
<div class="toolbar-left">
|
|
7018
|
-
<button type="button" class="icon-toggle nav-toggle" data-ref="nav-btn" title="Toggle contents" aria-label="Toggle contents">
|
|
7019
|
-
${NAV_TOGGLE_ICON_HTML}
|
|
7020
|
-
</button>
|
|
7021
|
-
</div>
|
|
7022
|
-
|
|
7023
|
-
<div class="edit-toolbar" data-ref="edit-toolbar" role="toolbar" aria-label="Markdown formatting">
|
|
7024
|
-
<div class="edit-toolbar-group">
|
|
7025
|
-
<button type="button" data-format="bold" data-format-control="bold" title="Bold" aria-label="Bold">${BOLD_ICON_HTML}</button>
|
|
7026
|
-
<button type="button" data-format="italic" data-format-control="italic" title="Italic" aria-label="Italic">${ITALIC_ICON_HTML}</button>
|
|
7027
|
-
<button type="button" data-format="strike" data-format-control="strikethrough" title="Strikethrough" aria-label="Strikethrough">${STRIKE_ICON_HTML}</button>
|
|
7028
|
-
<div class="format-menu" data-format-control="headings">
|
|
7029
|
-
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
7030
|
-
aria-label="Heading" aria-haspopup="menu" aria-expanded="false">
|
|
7031
|
-
${HEADING_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
7032
|
-
</button>
|
|
7033
|
-
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Heading level" hidden>
|
|
7034
|
-
<button type="button" role="menuitem" data-format="paragraph">Paragraph</button>
|
|
7035
|
-
<button type="button" role="menuitem" data-format="heading-1" data-heading-level="1"><strong>H1</strong> Heading 1</button>
|
|
7036
|
-
<button type="button" role="menuitem" data-format="heading-2" data-heading-level="2"><strong>H2</strong> Heading 2</button>
|
|
7037
|
-
<button type="button" role="menuitem" data-format="heading-3" data-heading-level="3"><strong>H3</strong> Heading 3</button>
|
|
7038
|
-
<button type="button" role="menuitem" data-format="heading-4" data-heading-level="4"><strong>H4</strong> Heading 4</button>
|
|
7039
|
-
<button type="button" role="menuitem" data-format="heading-5" data-heading-level="5"><strong>H5</strong> Heading 5</button>
|
|
7040
|
-
<button type="button" role="menuitem" data-format="heading-6" data-heading-level="6"><strong>H6</strong> Heading 6</button>
|
|
7041
|
-
</div>
|
|
7042
|
-
</div>
|
|
7043
|
-
</div>
|
|
7044
|
-
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
7045
|
-
<div class="edit-toolbar-group">
|
|
7046
|
-
<button type="button" data-format="bullet-list" data-format-control="bulletList" title="Bulleted list" aria-label="Bulleted list">${BULLET_LIST_ICON_HTML}</button>
|
|
7047
|
-
<button type="button" data-format="ordered-list" data-format-control="orderedList" title="Numbered list" aria-label="Numbered list">${ORDERED_LIST_ICON_HTML}</button>
|
|
7048
|
-
<div class="format-menu" data-format-control="code">
|
|
7049
|
-
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
7050
|
-
aria-label="Code" aria-haspopup="menu" aria-expanded="false">
|
|
7051
|
-
${CODE_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
7052
|
-
</button>
|
|
7053
|
-
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Code format" hidden>
|
|
7054
|
-
<button type="button" role="menuitem" data-format="code" data-code-kind="inline">Inline code</button>
|
|
7055
|
-
<button type="button" role="menuitem" data-format="code-block" data-code-kind="block">Code block</button>
|
|
7056
|
-
</div>
|
|
7057
|
-
</div>
|
|
7058
|
-
<button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
|
|
7059
|
-
<button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
|
|
7060
|
-
</div>
|
|
7061
|
-
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
7062
|
-
<div class="edit-toolbar-group">
|
|
7063
|
-
<button type="button" data-format="link" data-format-control="link" title="Link" aria-label="Link">${LINK_ICON_HTML}</button>
|
|
7064
|
-
<button type="button" data-format="image" data-format-control="image" title="Image" aria-label="Image">${IMAGE_FORMAT_ICON_HTML}</button>
|
|
7065
|
-
<input type="file" data-ref="image-input" accept="image/*" hidden />
|
|
7066
|
-
</div>
|
|
7067
|
-
</div>
|
|
7068
|
-
</div>
|
|
7069
|
-
|
|
7070
|
-
<div class="toolbar-buttons view-mode-toggle-group" data-ref="layout-controls" role="group" aria-label="Editor layout">
|
|
7071
|
-
<button type="button" class="icon-toggle view-mode-toggle" data-ref="source-btn" title="Edit" aria-label="Edit" aria-pressed="false">
|
|
7072
|
-
<span class="commandbar-flex-container" data-ref="source-icon">${SOURCE_EDIT_ICON_HTML}</span>
|
|
7073
|
-
</button>
|
|
7074
|
-
<button type="button" class="icon-toggle view-mode-toggle" data-ref="split-btn" title="Split" aria-label="Split" aria-pressed="false">
|
|
7075
|
-
<span class="commandbar-flex-container">
|
|
7076
|
-
${SPLIT_ICON_HTML}
|
|
7077
|
-
</span>
|
|
7078
|
-
</button>
|
|
7079
|
-
<button type="button" class="icon-toggle view-mode-toggle" data-ref="preview-btn" title="View" aria-label="View" aria-pressed="false">
|
|
7080
|
-
<span class="commandbar-flex-container">
|
|
7081
|
-
${PREVIEW_ICON_HTML}
|
|
7082
|
-
</span>
|
|
7083
|
-
</button>
|
|
7084
|
-
</div>
|
|
7085
|
-
|
|
7086
|
-
<div class="toolbar-controls" data-ref="toolbar-controls">
|
|
7087
|
-
<button type="button" class="icon-toggle" data-ref="search-btn" title="Find/replace (Mod-F)" aria-label="Find/replace">
|
|
7088
|
-
${SEARCH_ICON_HTML}
|
|
7089
|
-
</button>
|
|
7090
|
-
<button type="button" class="icon-toggle" data-ref="save-btn" title="Save" aria-label="Save">
|
|
7091
|
-
${SAVE_ICON_HTML}
|
|
7092
|
-
</button>
|
|
7093
|
-
<button type="button" class="icon-toggle zoom-toggle" data-ref="zoom-btn" title="Zoom controls" aria-label="Zoom controls">
|
|
7094
|
-
${ZOOM_ICON_HTML}
|
|
7095
|
-
</button>
|
|
7096
|
-
<div class="
|
|
7097
|
-
<button type="button" class="icon-toggle
|
|
7098
|
-
title="
|
|
7099
|
-
|
|
7100
|
-
</button>
|
|
7101
|
-
<
|
|
7102
|
-
|
|
7103
|
-
${
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
<
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
<
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
<
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
<
|
|
7230
|
-
<
|
|
7231
|
-
<
|
|
7232
|
-
<
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
<
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
<
|
|
7260
|
-
<
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
<button type="button" data-action="cancel-
|
|
7271
|
-
<button type="button" class="save-title" data-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
<
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
<
|
|
7307
|
-
<
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
</
|
|
7767
|
+
const SHELL_HTML = `
|
|
7768
|
+
<section class="mdzip-root">
|
|
7769
|
+
<div class="document-strip" data-ref="document-strip" hidden>
|
|
7770
|
+
<button type="button" class="title-button" data-ref="title-btn"></button>
|
|
7771
|
+
<button type="button" class="document-info-button" data-ref="document-info-btn"
|
|
7772
|
+
title="Document information" aria-label="Document information">
|
|
7773
|
+
${INFO_ICON_HTML}
|
|
7774
|
+
</button>
|
|
7775
|
+
</div>
|
|
7776
|
+
|
|
7777
|
+
<header class="toolbar" data-ref="toolbar" hidden>
|
|
7778
|
+
<div class="toolbar-start">
|
|
7779
|
+
<div class="toolbar-left">
|
|
7780
|
+
<button type="button" class="icon-toggle nav-toggle" data-ref="nav-btn" title="Toggle contents" aria-label="Toggle contents">
|
|
7781
|
+
${NAV_TOGGLE_ICON_HTML}
|
|
7782
|
+
</button>
|
|
7783
|
+
</div>
|
|
7784
|
+
|
|
7785
|
+
<div class="edit-toolbar" data-ref="edit-toolbar" role="toolbar" aria-label="Markdown formatting">
|
|
7786
|
+
<div class="edit-toolbar-group">
|
|
7787
|
+
<button type="button" data-format="bold" data-format-control="bold" title="Bold" aria-label="Bold">${BOLD_ICON_HTML}</button>
|
|
7788
|
+
<button type="button" data-format="italic" data-format-control="italic" title="Italic" aria-label="Italic">${ITALIC_ICON_HTML}</button>
|
|
7789
|
+
<button type="button" data-format="strike" data-format-control="strikethrough" title="Strikethrough" aria-label="Strikethrough">${STRIKE_ICON_HTML}</button>
|
|
7790
|
+
<div class="format-menu" data-format-control="headings">
|
|
7791
|
+
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
7792
|
+
aria-label="Heading" aria-haspopup="menu" aria-expanded="false">
|
|
7793
|
+
${HEADING_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
7794
|
+
</button>
|
|
7795
|
+
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Heading level" hidden>
|
|
7796
|
+
<button type="button" role="menuitem" data-format="paragraph">Paragraph</button>
|
|
7797
|
+
<button type="button" role="menuitem" data-format="heading-1" data-heading-level="1"><strong>H1</strong> Heading 1</button>
|
|
7798
|
+
<button type="button" role="menuitem" data-format="heading-2" data-heading-level="2"><strong>H2</strong> Heading 2</button>
|
|
7799
|
+
<button type="button" role="menuitem" data-format="heading-3" data-heading-level="3"><strong>H3</strong> Heading 3</button>
|
|
7800
|
+
<button type="button" role="menuitem" data-format="heading-4" data-heading-level="4"><strong>H4</strong> Heading 4</button>
|
|
7801
|
+
<button type="button" role="menuitem" data-format="heading-5" data-heading-level="5"><strong>H5</strong> Heading 5</button>
|
|
7802
|
+
<button type="button" role="menuitem" data-format="heading-6" data-heading-level="6"><strong>H6</strong> Heading 6</button>
|
|
7803
|
+
</div>
|
|
7804
|
+
</div>
|
|
7805
|
+
</div>
|
|
7806
|
+
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
7807
|
+
<div class="edit-toolbar-group">
|
|
7808
|
+
<button type="button" data-format="bullet-list" data-format-control="bulletList" title="Bulleted list" aria-label="Bulleted list">${BULLET_LIST_ICON_HTML}</button>
|
|
7809
|
+
<button type="button" data-format="ordered-list" data-format-control="orderedList" title="Numbered list" aria-label="Numbered list">${ORDERED_LIST_ICON_HTML}</button>
|
|
7810
|
+
<div class="format-menu" data-format-control="code">
|
|
7811
|
+
<button type="button" class="format-menu-toggle" data-format-menu-toggle
|
|
7812
|
+
aria-label="Code" aria-haspopup="menu" aria-expanded="false">
|
|
7813
|
+
${CODE_ICON_HTML}${CHEVRON_ICON_HTML}
|
|
7814
|
+
</button>
|
|
7815
|
+
<div class="format-menu-popover" data-format-menu role="menu" aria-label="Code format" hidden>
|
|
7816
|
+
<button type="button" role="menuitem" data-format="code" data-code-kind="inline">Inline code</button>
|
|
7817
|
+
<button type="button" role="menuitem" data-format="code-block" data-code-kind="block">Code block</button>
|
|
7818
|
+
</div>
|
|
7819
|
+
</div>
|
|
7820
|
+
<button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
|
|
7821
|
+
<button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
|
|
7822
|
+
</div>
|
|
7823
|
+
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
7824
|
+
<div class="edit-toolbar-group">
|
|
7825
|
+
<button type="button" data-format="link" data-format-control="link" title="Link" aria-label="Link">${LINK_ICON_HTML}</button>
|
|
7826
|
+
<button type="button" data-format="image" data-format-control="image" title="Image" aria-label="Image">${IMAGE_FORMAT_ICON_HTML}</button>
|
|
7827
|
+
<input type="file" data-ref="image-input" accept="image/*" hidden />
|
|
7828
|
+
</div>
|
|
7829
|
+
</div>
|
|
7830
|
+
</div>
|
|
7831
|
+
|
|
7832
|
+
<div class="toolbar-buttons view-mode-toggle-group" data-ref="layout-controls" role="group" aria-label="Editor layout">
|
|
7833
|
+
<button type="button" class="icon-toggle view-mode-toggle" data-ref="source-btn" title="Edit" aria-label="Edit" aria-pressed="false">
|
|
7834
|
+
<span class="commandbar-flex-container" data-ref="source-icon">${SOURCE_EDIT_ICON_HTML}</span>
|
|
7835
|
+
</button>
|
|
7836
|
+
<button type="button" class="icon-toggle view-mode-toggle" data-ref="split-btn" title="Split" aria-label="Split" aria-pressed="false">
|
|
7837
|
+
<span class="commandbar-flex-container">
|
|
7838
|
+
${SPLIT_ICON_HTML}
|
|
7839
|
+
</span>
|
|
7840
|
+
</button>
|
|
7841
|
+
<button type="button" class="icon-toggle view-mode-toggle" data-ref="preview-btn" title="View" aria-label="View" aria-pressed="false">
|
|
7842
|
+
<span class="commandbar-flex-container">
|
|
7843
|
+
${PREVIEW_ICON_HTML}
|
|
7844
|
+
</span>
|
|
7845
|
+
</button>
|
|
7846
|
+
</div>
|
|
7847
|
+
|
|
7848
|
+
<div class="toolbar-controls" data-ref="toolbar-controls">
|
|
7849
|
+
<button type="button" class="icon-toggle" data-ref="search-btn" title="Find/replace (Mod-F)" aria-label="Find/replace">
|
|
7850
|
+
${SEARCH_ICON_HTML}
|
|
7851
|
+
</button>
|
|
7852
|
+
<button type="button" class="icon-toggle" data-ref="save-btn" title="Save" aria-label="Save">
|
|
7853
|
+
${SAVE_ICON_HTML}
|
|
7854
|
+
</button>
|
|
7855
|
+
<button type="button" class="icon-toggle zoom-toggle" data-ref="zoom-btn" title="Zoom controls" aria-label="Zoom controls">
|
|
7856
|
+
${ZOOM_ICON_HTML}
|
|
7857
|
+
</button>
|
|
7858
|
+
<div class="toolbar-menu width-menu" data-ref="width-controls">
|
|
7859
|
+
<button type="button" class="icon-toggle toolbar-menu-toggle" data-ref="width-menu-btn"
|
|
7860
|
+
title="Page width" aria-label="Page width" aria-haspopup="menu" aria-expanded="false">
|
|
7861
|
+
<span class="toolbar-menu-icon" data-ref="width-menu-icon">${PAGE_WIDTH_DEFAULT_ICON_HTML}</span>
|
|
7862
|
+
</button>
|
|
7863
|
+
<div class="toolbar-popover" data-ref="width-popover" role="menu" aria-label="Page width" hidden>
|
|
7864
|
+
${['narrow', 'default', 'wide', 'full'].map((choice) => `
|
|
7865
|
+
<button type="button" role="menuitemradio" class="icon-toggle toolbar-menu-item" data-width="${choice}"
|
|
7866
|
+
title="${PREVIEW_WIDTH_LABELS[choice]}" aria-label="${PREVIEW_WIDTH_LABELS[choice]}" aria-checked="false">
|
|
7867
|
+
${PREVIEW_WIDTH_ICONS_HTML[choice]}
|
|
7868
|
+
</button>`).join('')}
|
|
7869
|
+
</div>
|
|
7870
|
+
</div>
|
|
7871
|
+
<div class="theme-menu" data-ref="theme-controls">
|
|
7872
|
+
<button type="button" class="icon-toggle theme-menu-toggle" data-ref="theme-menu-btn"
|
|
7873
|
+
title="Color scheme" aria-label="Color scheme" aria-haspopup="menu" aria-expanded="false">
|
|
7874
|
+
<span class="theme-menu-icon" data-ref="theme-menu-icon">${SYSTEM_THEME_ICON_HTML}</span>
|
|
7875
|
+
</button>
|
|
7876
|
+
<div class="theme-popover" data-ref="theme-popover" role="menu" aria-label="Color scheme" hidden>
|
|
7877
|
+
<button type="button" role="menuitemradio" class="icon-toggle theme-toggle" data-ref="light-theme-btn"
|
|
7878
|
+
title="Light mode" aria-label="Light mode" aria-checked="false">
|
|
7879
|
+
${LIGHT_THEME_ICON_HTML}
|
|
7880
|
+
</button>
|
|
7881
|
+
<button type="button" role="menuitemradio" class="icon-toggle theme-toggle" data-ref="dark-theme-btn"
|
|
7882
|
+
title="Dark mode" aria-label="Dark mode" aria-checked="false">
|
|
7883
|
+
${DARK_THEME_ICON_HTML}
|
|
7884
|
+
</button>
|
|
7885
|
+
<button type="button" role="menuitemradio" class="icon-toggle theme-toggle" data-ref="system-theme-btn"
|
|
7886
|
+
title="System setting" aria-label="System setting" aria-checked="false">
|
|
7887
|
+
${SYSTEM_THEME_ICON_HTML}
|
|
7888
|
+
</button>
|
|
7889
|
+
</div>
|
|
7890
|
+
</div>
|
|
7891
|
+
<div class="zoom-popover" data-ref="zoom-popover" hidden role="group" aria-label="Zoom">
|
|
7892
|
+
<span class="zoom-level" data-ref="zoom-level">100%</span>
|
|
7893
|
+
<span class="zoom-stepper">
|
|
7894
|
+
<button type="button" data-action="zoom-out" title="Zoom out" aria-label="Zoom out">-</button>
|
|
7895
|
+
<button type="button" data-action="zoom-in" title="Zoom in" aria-label="Zoom in">+</button>
|
|
7896
|
+
</span>
|
|
7897
|
+
<button type="button" class="zoom-reset" data-action="zoom-reset" title="Reset zoom">Reset</button>
|
|
7898
|
+
</div>
|
|
7899
|
+
</div>
|
|
7900
|
+
</header>
|
|
7901
|
+
|
|
7902
|
+
<div class="workspace-shell" data-ref="workspace-shell" hidden>
|
|
7903
|
+
<aside class="nav-pane" data-ref="nav-pane" aria-label="Package contents">
|
|
7904
|
+
<div class="nav-tree" data-ref="nav-tree"></div>
|
|
7905
|
+
</aside>
|
|
7906
|
+
<div class="nav-resizer" data-ref="nav-resizer"
|
|
7907
|
+
role="separator" aria-orientation="vertical" aria-label="Resize contents pane"></div>
|
|
7908
|
+
|
|
7909
|
+
<div class="pane-stack" data-ref="pane-stack">
|
|
7910
|
+
<section class="pane edit-pane" data-ref="edit-pane">
|
|
7911
|
+
<div class="editor-host" data-ref="editor-host"></div>
|
|
7912
|
+
</section>
|
|
7913
|
+
<div class="split-resizer" data-ref="split-resizer"
|
|
7914
|
+
role="separator" aria-orientation="vertical" aria-label="Resize split panes"></div>
|
|
7915
|
+
<section class="pane preview-pane" data-ref="preview-pane" tabindex="-1">
|
|
7916
|
+
<article class="preview-content" data-ref="preview-content"></article>
|
|
7917
|
+
</section>
|
|
7918
|
+
<section class="pane entry-pane" data-ref="entry-pane"></section>
|
|
7919
|
+
</div>
|
|
7920
|
+
</div>
|
|
7921
|
+
|
|
7922
|
+
<div class="title-dialog-backdrop" data-ref="title-dialog" hidden
|
|
7923
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-title-dialog-heading">
|
|
7924
|
+
<div class="title-dialog">
|
|
7925
|
+
<h3 id="mdzip-title-dialog-heading">Set Document Title</h3>
|
|
7926
|
+
<p>This is the package-level title stored in manifest.json.</p>
|
|
7927
|
+
<input type="text" maxlength="120" data-ref="title-input" aria-label="Document title" />
|
|
7928
|
+
<p class="title-dialog-validation" data-ref="title-validation" hidden>Title cannot be empty.</p>
|
|
7929
|
+
<p>If unset, consumers may fall back to entry point, filename, or first heading.</p>
|
|
7930
|
+
<div class="title-dialog-actions">
|
|
7931
|
+
<button type="button" class="reset-title" data-ref="title-reset-btn">Reset</button>
|
|
7932
|
+
<button type="button" data-action="cancel-title">Cancel</button>
|
|
7933
|
+
<button type="button" class="save-title" data-ref="title-save-btn">Save</button>
|
|
7934
|
+
</div>
|
|
7935
|
+
</div>
|
|
7936
|
+
</div>
|
|
7937
|
+
|
|
7938
|
+
<div class="title-dialog-backdrop" data-ref="conversion-dialog" hidden
|
|
7939
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-conversion-dialog-heading">
|
|
7940
|
+
<div class="title-dialog">
|
|
7941
|
+
<h3 id="mdzip-conversion-dialog-heading">Convert to MDZ?</h3>
|
|
7942
|
+
<p>
|
|
7943
|
+
Regular Markdown files contain only text. Convert this document to an
|
|
7944
|
+
MDZ package to add images, package navigation, and document metadata.
|
|
7945
|
+
</p>
|
|
7946
|
+
<p>The next save will produce an .mdz file.</p>
|
|
7947
|
+
<div class="title-dialog-actions">
|
|
7948
|
+
<button type="button" data-action="cancel-conversion">Cancel</button>
|
|
7949
|
+
<button type="button" class="save-title" data-ref="conversion-confirm-btn">Convert</button>
|
|
7950
|
+
</div>
|
|
7951
|
+
</div>
|
|
7952
|
+
</div>
|
|
7953
|
+
|
|
7954
|
+
<div class="title-dialog-backdrop" data-ref="image-insert-dialog" hidden
|
|
7955
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-image-insert-dialog-heading">
|
|
7956
|
+
<div class="title-dialog image-insert-dialog">
|
|
7957
|
+
<h3 id="mdzip-image-insert-dialog-heading">Insert Image</h3>
|
|
7958
|
+
<p>Choose the Markdown inserted for this image.</p>
|
|
7959
|
+
<fieldset class="image-insert-options">
|
|
7960
|
+
<legend>Output</legend>
|
|
7961
|
+
<label>
|
|
7962
|
+
<input type="radio" name="mdzip-image-insert-mode" value="markdown"
|
|
7963
|
+
data-ref="image-insert-mode-markdown" checked />
|
|
7964
|
+
Markdown image
|
|
7965
|
+
</label>
|
|
7966
|
+
<label>
|
|
7967
|
+
<input type="radio" name="mdzip-image-insert-mode" value="html"
|
|
7968
|
+
data-ref="image-insert-mode-html" />
|
|
7969
|
+
HTML <img> with sizing
|
|
7970
|
+
</label>
|
|
7971
|
+
</fieldset>
|
|
7972
|
+
<label class="image-insert-field">
|
|
7973
|
+
<span>Alt text</span>
|
|
7974
|
+
<input type="text" data-ref="image-insert-alt" />
|
|
7975
|
+
</label>
|
|
7976
|
+
<div class="image-insert-grid">
|
|
7977
|
+
<label class="image-insert-field">
|
|
7978
|
+
<span>Size by</span>
|
|
7979
|
+
<select data-ref="image-insert-size-mode">
|
|
7980
|
+
<option value="original">Original size</option>
|
|
7981
|
+
<option value="width" selected>Width</option>
|
|
7982
|
+
<option value="height">Height</option>
|
|
7983
|
+
<option value="percent">Percent</option>
|
|
7984
|
+
</select>
|
|
7985
|
+
</label>
|
|
7986
|
+
<label class="image-insert-field">
|
|
7987
|
+
<span>Value</span>
|
|
7988
|
+
<input type="number" min="1" step="1" data-ref="image-insert-size-value" />
|
|
7989
|
+
</label>
|
|
7990
|
+
</div>
|
|
7991
|
+
<label class="image-insert-field">
|
|
7992
|
+
<span>Position</span>
|
|
7993
|
+
<select data-ref="image-insert-position">
|
|
7994
|
+
<option value="inline">Inline/default</option>
|
|
7995
|
+
<option value="left">Left</option>
|
|
7996
|
+
<option value="center">Center</option>
|
|
7997
|
+
<option value="right">Right</option>
|
|
7998
|
+
<option value="wrap-left">Wrap left</option>
|
|
7999
|
+
<option value="wrap-right">Wrap right</option>
|
|
8000
|
+
</select>
|
|
8001
|
+
</label>
|
|
8002
|
+
<div class="title-dialog-actions">
|
|
8003
|
+
<button type="button" data-action="cancel-image-insert">Cancel</button>
|
|
8004
|
+
<button type="button" class="save-title" data-ref="image-insert-confirm-btn">Insert</button>
|
|
8005
|
+
</div>
|
|
8006
|
+
</div>
|
|
8007
|
+
</div>
|
|
8008
|
+
|
|
8009
|
+
<div class="title-dialog-backdrop" data-ref="pack-files-dialog" hidden
|
|
8010
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-pack-files-dialog-heading">
|
|
8011
|
+
<div class="title-dialog pack-files-dialog">
|
|
8012
|
+
<h3 id="mdzip-pack-files-dialog-heading">Package Markdown Files</h3>
|
|
8013
|
+
<p>These files contain more than one Markdown document. Choose how to package them.</p>
|
|
8014
|
+
<fieldset class="pack-files-options">
|
|
8015
|
+
<legend>Mode</legend>
|
|
8016
|
+
<label>
|
|
8017
|
+
<input type="radio" name="mdzip-pack-files-mode" value="document"
|
|
8018
|
+
data-ref="pack-files-mode-document" checked />
|
|
8019
|
+
Document — one primary page; the rest are attached
|
|
8020
|
+
</label>
|
|
8021
|
+
<label>
|
|
8022
|
+
<input type="radio" name="mdzip-pack-files-mode" value="project"
|
|
8023
|
+
data-ref="pack-files-mode-project" />
|
|
8024
|
+
Project — a set of related documents
|
|
8025
|
+
</label>
|
|
8026
|
+
</fieldset>
|
|
8027
|
+
<label class="pack-files-field">
|
|
8028
|
+
<span>Entry point</span>
|
|
8029
|
+
<select data-ref="pack-files-entry"></select>
|
|
8030
|
+
</label>
|
|
8031
|
+
<div class="title-dialog-actions">
|
|
8032
|
+
<button type="button" data-action="cancel-pack-files">Cancel</button>
|
|
8033
|
+
<button type="button" class="save-title" data-ref="pack-files-confirm-btn">Package</button>
|
|
8034
|
+
</div>
|
|
8035
|
+
</div>
|
|
8036
|
+
</div>
|
|
8037
|
+
|
|
8038
|
+
<div class="title-dialog-backdrop" data-ref="copy-render-dialog" hidden
|
|
8039
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-copy-render-dialog-heading">
|
|
8040
|
+
<div class="title-dialog copy-render-dialog">
|
|
8041
|
+
<h3 id="mdzip-copy-render-dialog-heading" data-ref="copy-render-heading">Copying document...</h3>
|
|
8042
|
+
<div data-ref="copy-render-progress-section">
|
|
8043
|
+
<p data-ref="copy-render-progress-text">Rendering the full document...</p>
|
|
8044
|
+
<div class="copy-render-progress-track">
|
|
8045
|
+
<div class="copy-render-progress-bar" data-ref="copy-render-progress-bar"></div>
|
|
8046
|
+
</div>
|
|
8047
|
+
</div>
|
|
8048
|
+
<p data-ref="copy-render-ready-text" hidden>
|
|
8049
|
+
The document is ready. Browsers only allow a clipboard write right after a click, so this needs one more —
|
|
8050
|
+
click Copy to finish.
|
|
8051
|
+
</p>
|
|
8052
|
+
<p data-ref="copy-render-done-text" hidden></p>
|
|
8053
|
+
<div class="title-dialog-actions">
|
|
8054
|
+
<button type="button" data-action="cancel-copy-render" data-ref="copy-render-cancel-btn">Cancel</button>
|
|
8055
|
+
<button type="button" data-action="cancel-copy-ready" data-ref="copy-render-ready-cancel-btn" hidden>Cancel</button>
|
|
8056
|
+
<button type="button" class="save-title" data-action="confirm-copy-ready" data-ref="copy-render-confirm-btn" hidden>Copy</button>
|
|
8057
|
+
<button type="button" class="save-title" data-action="dismiss-copy-render" data-ref="copy-render-dismiss-btn" hidden>Dismiss</button>
|
|
8058
|
+
</div>
|
|
8059
|
+
</div>
|
|
8060
|
+
</div>
|
|
8061
|
+
|
|
8062
|
+
<div class="title-dialog-backdrop" data-ref="metadata-dialog" hidden
|
|
8063
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-metadata-dialog-heading">
|
|
8064
|
+
<div class="title-dialog metadata-dialog">
|
|
8065
|
+
<h3 id="mdzip-metadata-dialog-heading">Document Information</h3>
|
|
8066
|
+
<dl data-ref="metadata-list"></dl>
|
|
8067
|
+
<h4>Libraries</h4>
|
|
8068
|
+
<dl data-ref="library-list"></dl>
|
|
8069
|
+
<div class="title-dialog-actions">
|
|
8070
|
+
<button type="button" class="save-title" data-action="close-metadata">Close</button>
|
|
8071
|
+
</div>
|
|
8072
|
+
</div>
|
|
8073
|
+
</div>
|
|
8074
|
+
|
|
8075
|
+
<div class="title-dialog-backdrop" data-ref="name-dialog" hidden
|
|
8076
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-name-dialog-heading">
|
|
8077
|
+
<div class="title-dialog">
|
|
8078
|
+
<h3 id="mdzip-name-dialog-heading" data-ref="name-dialog-heading">New Markdown File</h3>
|
|
8079
|
+
<input type="text" maxlength="260" data-ref="name-input" aria-label="File name" />
|
|
8080
|
+
<p class="title-dialog-validation" data-ref="name-validation" hidden></p>
|
|
8081
|
+
<div class="title-dialog-actions">
|
|
8082
|
+
<button type="button" data-action="cancel-name">Cancel</button>
|
|
8083
|
+
<button type="button" class="save-title" data-ref="name-confirm-btn">Create</button>
|
|
8084
|
+
</div>
|
|
8085
|
+
</div>
|
|
8086
|
+
</div>
|
|
8087
|
+
|
|
8088
|
+
<div class="title-dialog-backdrop" data-ref="delete-dialog" hidden
|
|
8089
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-delete-dialog-heading">
|
|
8090
|
+
<div class="title-dialog">
|
|
8091
|
+
<h3 id="mdzip-delete-dialog-heading">Delete File?</h3>
|
|
8092
|
+
<p data-ref="delete-dialog-text"></p>
|
|
8093
|
+
<div class="title-dialog-actions">
|
|
8094
|
+
<button type="button" data-action="cancel-delete">Cancel</button>
|
|
8095
|
+
<button type="button" class="danger-action" data-ref="delete-confirm-btn">Delete</button>
|
|
8096
|
+
</div>
|
|
8097
|
+
</div>
|
|
8098
|
+
</div>
|
|
8099
|
+
|
|
8100
|
+
<input type="file" data-ref="replace-input" hidden />
|
|
8101
|
+
|
|
8102
|
+
<div class="nav-context-menu" data-ref="nav-menu" hidden role="menu"></div>
|
|
8103
|
+
|
|
8104
|
+
<div class="mdzip-tooltip" data-ref="tooltip" role="tooltip" hidden></div>
|
|
8105
|
+
|
|
8106
|
+
<div class="mdzip-copy-toast" data-ref="copy-toast" role="status" aria-live="polite" hidden></div>
|
|
8107
|
+
|
|
8108
|
+
<p class="mdzip-empty" data-ref="empty-state">No MDZip workspace loaded.</p>
|
|
8109
|
+
</section>
|
|
7325
8110
|
`;
|
|
7326
8111
|
//# sourceMappingURL=view.js.map
|