@mdzip/editor 1.3.11 → 1.3.13
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/README.md +63 -0
- package/dist/library-info.d.ts +2 -2
- package/dist/library-info.js +2 -2
- package/dist/view-css.d.ts.map +1 -1
- package/dist/view-css.js +317 -54
- package/dist/view-css.js.map +1 -1
- package/dist/view.d.ts +116 -3
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +965 -57
- package/dist/view.js.map +1 -1
- package/dist/workspace-view.d.ts +1 -1
- package/dist/workspace-view.d.ts.map +1 -1
- package/dist/workspace-view.js +38 -3
- package/dist/workspace-view.js.map +1 -1
- package/dist/workspace.d.ts +1 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +3 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +75 -75
package/dist/view.js
CHANGED
|
@@ -2,11 +2,11 @@ import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirro
|
|
|
2
2
|
import { markdown } from '@codemirror/lang-markdown';
|
|
3
3
|
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
4
4
|
import { Compartment, EditorState } from '@codemirror/state';
|
|
5
|
-
import { EditorView, dropCursor, keymap, lineNumbers } from '@codemirror/view';
|
|
5
|
+
import { Decoration, EditorView, MatchDecorator, ViewPlugin, dropCursor, keymap, lineNumbers } from '@codemirror/view';
|
|
6
6
|
import { tags } from '@lezer/highlight';
|
|
7
|
-
import { Bold, ChevronDown, Code, Columns2, Eye, File, FileBraces, FileImage, Folder, FolderOpen, Hash, Heading1, ImagePlus, Info, Italic, Link2Off, Link, List, ListOrdered, Moon, PanelLeft, Quote, Save, SquarePen, Strikethrough, Sun, ZoomIn } from 'lucide';
|
|
7
|
+
import { Bold, 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, SquareCode, SquarePen, Strikethrough, Sun, TextSelect, ZoomIn } from 'lucide';
|
|
8
8
|
import { browserClipboardHasImage, readBrowserClipboardImage } from './browser.js';
|
|
9
|
-
import { MdzipAssetSession, mdzipArchiveSourceId } from './asset-cache.js';
|
|
9
|
+
import { MdzipAssetSession, mdzipArchiveSourceId, sniffImageSize } from './asset-cache.js';
|
|
10
10
|
import { MD_MARKDOWN_ICON } from './icons/md-markdown.js';
|
|
11
11
|
import { MDZIP_RUNTIME_LIBRARIES } from './library-info.js';
|
|
12
12
|
import { MdzipWorkspaceService, extensionForMime, normalizeArchivePath, relativeArchivePath } from './workspace.js';
|
|
@@ -31,6 +31,7 @@ const ORPHAN_ICON_HTML = lucideIcon(Link2Off, '');
|
|
|
31
31
|
const SOURCE_EDIT_ICON_HTML = lucideIcon(SquarePen, TOOLBAR_ICON_CLASS);
|
|
32
32
|
const SOURCE_MARKDOWN_ICON_HTML = lucideIcon(Hash, TOOLBAR_ICON_CLASS);
|
|
33
33
|
const NAV_TOGGLE_ICON_HTML = lucideIcon(PanelLeft, `${TOOLBAR_ICON_CLASS} nav-toggle-icon`);
|
|
34
|
+
const CONVERT_TO_MDZ_ICON_HTML = lucideIcon(PackagePlus, `${TOOLBAR_ICON_CLASS} convert-mdz-icon`);
|
|
34
35
|
const PREVIEW_ICON_HTML = lucideIcon(Eye, TOOLBAR_ICON_CLASS);
|
|
35
36
|
const SPLIT_ICON_HTML = lucideIcon(Columns2, TOOLBAR_ICON_CLASS);
|
|
36
37
|
const SAVE_ICON_HTML = lucideIcon(Save, TOOLBAR_ICON_CLASS);
|
|
@@ -46,10 +47,91 @@ const BULLET_LIST_ICON_HTML = lucideIcon(List, FORMAT_ICON_CLASS);
|
|
|
46
47
|
const ORDERED_LIST_ICON_HTML = lucideIcon(ListOrdered, FORMAT_ICON_CLASS);
|
|
47
48
|
const CODE_ICON_HTML = lucideIcon(Code, FORMAT_ICON_CLASS);
|
|
48
49
|
const QUOTE_ICON_HTML = lucideIcon(Quote, FORMAT_ICON_CLASS);
|
|
50
|
+
const LINE_BREAK_ICON_HTML = lucideIcon(CornerDownLeft, FORMAT_ICON_CLASS);
|
|
49
51
|
const LINK_ICON_HTML = lucideIcon(Link, FORMAT_ICON_CLASS);
|
|
50
52
|
const IMAGE_FORMAT_ICON_HTML = lucideIcon(ImagePlus, FORMAT_ICON_CLASS);
|
|
51
53
|
const CHEVRON_ICON_HTML = lucideIcon(ChevronDown, 'format-chevron');
|
|
52
54
|
const INFO_ICON_HTML = lucideIcon(Info, 'document-info-icon');
|
|
55
|
+
// Leading icons for context-menu items (Obsidian-style icon column).
|
|
56
|
+
const MENU_ICON_CLASS = 'nav-menu-icon';
|
|
57
|
+
const MENU_CUT_ICON_HTML = lucideIcon(Scissors, MENU_ICON_CLASS);
|
|
58
|
+
const MENU_COPY_ICON_HTML = lucideIcon(Copy, MENU_ICON_CLASS);
|
|
59
|
+
const MENU_PASTE_ICON_HTML = lucideIcon(ClipboardPaste, MENU_ICON_CLASS);
|
|
60
|
+
const MENU_PASTE_PLAIN_ICON_HTML = lucideIcon(ClipboardType, MENU_ICON_CLASS);
|
|
61
|
+
const MENU_SELECT_ALL_ICON_HTML = lucideIcon(TextSelect, MENU_ICON_CLASS);
|
|
62
|
+
const MENU_BOLD_ICON_HTML = lucideIcon(Bold, MENU_ICON_CLASS);
|
|
63
|
+
const MENU_ITALIC_ICON_HTML = lucideIcon(Italic, MENU_ICON_CLASS);
|
|
64
|
+
const MENU_STRIKE_ICON_HTML = lucideIcon(Strikethrough, MENU_ICON_CLASS);
|
|
65
|
+
const MENU_CODE_ICON_HTML = lucideIcon(Code, MENU_ICON_CLASS);
|
|
66
|
+
const MENU_CODE_BLOCK_ICON_HTML = lucideIcon(SquareCode, MENU_ICON_CLASS);
|
|
67
|
+
const MENU_HIGHLIGHT_ICON_HTML = lucideIcon(Highlighter, MENU_ICON_CLASS);
|
|
68
|
+
const MENU_BULLET_LIST_ICON_HTML = lucideIcon(List, MENU_ICON_CLASS);
|
|
69
|
+
const MENU_ORDERED_LIST_ICON_HTML = lucideIcon(ListOrdered, MENU_ICON_CLASS);
|
|
70
|
+
const MENU_QUOTE_ICON_HTML = lucideIcon(Quote, MENU_ICON_CLASS);
|
|
71
|
+
const MENU_LINE_BREAK_ICON_HTML = lucideIcon(CornerDownLeft, MENU_ICON_CLASS);
|
|
72
|
+
const MENU_IMAGE_ICON_HTML = lucideIcon(ImagePlus, MENU_ICON_CLASS);
|
|
73
|
+
const MENU_LINK_ICON_HTML = lucideIcon(Link, MENU_ICON_CLASS);
|
|
74
|
+
const MENU_CLEAR_FORMAT_ICON_HTML = lucideIcon(Eraser, MENU_ICON_CLASS);
|
|
75
|
+
const MENU_HEADING_PARENT_ICON_HTML = lucideIcon(Heading1, MENU_ICON_CLASS);
|
|
76
|
+
const MENU_PARAGRAPH_ICON_HTML = lucideIcon(Pilcrow, MENU_ICON_CLASS);
|
|
77
|
+
const MENU_CHEVRON_ICON_HTML = lucideIcon(ChevronRight, 'nav-menu-chevron');
|
|
78
|
+
const MENU_HEADING_ICON_HTML = {
|
|
79
|
+
1: lucideIcon(Heading1, MENU_ICON_CLASS),
|
|
80
|
+
2: lucideIcon(Heading2, MENU_ICON_CLASS),
|
|
81
|
+
3: lucideIcon(Heading3, MENU_ICON_CLASS),
|
|
82
|
+
4: lucideIcon(Heading4, MENU_ICON_CLASS),
|
|
83
|
+
5: lucideIcon(Heading5, MENU_ICON_CLASS),
|
|
84
|
+
6: lucideIcon(Heading6, MENU_ICON_CLASS)
|
|
85
|
+
};
|
|
86
|
+
const MENU_SEPARATOR_HTML = '<div class="nav-menu-separator" role="separator"></div>';
|
|
87
|
+
// Matches the submenu `min-width` in the CSS; used to decide left/right flyout.
|
|
88
|
+
const SUBMENU_ESTIMATED_WIDTH = 190;
|
|
89
|
+
// Curated default for the Code Block submenu — a usable subset rather than the
|
|
90
|
+
// full highlight.js language set. Hosts override via `codeBlockLanguages`.
|
|
91
|
+
export const DEFAULT_CODE_BLOCK_LANGUAGES = [
|
|
92
|
+
{ id: '', label: 'Plain Text' },
|
|
93
|
+
{ id: 'typescript', label: 'TypeScript' },
|
|
94
|
+
{ id: 'javascript', label: 'JavaScript' },
|
|
95
|
+
{ id: 'tsx', label: 'TSX / JSX' },
|
|
96
|
+
{ id: 'html', label: 'HTML' },
|
|
97
|
+
{ id: 'css', label: 'CSS' },
|
|
98
|
+
{ id: 'json', label: 'JSON' },
|
|
99
|
+
{ id: 'python', label: 'Python' },
|
|
100
|
+
{ id: 'csharp', label: 'C#' },
|
|
101
|
+
{ id: 'java', label: 'Java' },
|
|
102
|
+
{ id: 'cpp', label: 'C++' },
|
|
103
|
+
{ id: 'go', label: 'Go' },
|
|
104
|
+
{ id: 'rust', label: 'Rust' },
|
|
105
|
+
{ id: 'sql', label: 'SQL' },
|
|
106
|
+
{ id: 'bash', label: 'Shell' },
|
|
107
|
+
{ id: 'yaml', label: 'YAML' },
|
|
108
|
+
{ id: 'markdown', label: 'Markdown' }
|
|
109
|
+
];
|
|
110
|
+
// Recursively renders a context-menu item. An item carrying `submenu` becomes a
|
|
111
|
+
// hover/focus flyout parent (no action of its own); everything else is an
|
|
112
|
+
// actionable `[data-menu-action]` button handled by the shared click delegate.
|
|
113
|
+
function renderContextMenuItem(item) {
|
|
114
|
+
const icon = item.icon ?? '';
|
|
115
|
+
const label = `<span class="nav-menu-label">${escapeHtml(item.label)}</span>`;
|
|
116
|
+
if (item.submenu) {
|
|
117
|
+
const children = item.submenu
|
|
118
|
+
.map((child) => (child === null ? MENU_SEPARATOR_HTML : renderContextMenuItem(child)))
|
|
119
|
+
.join('');
|
|
120
|
+
return '<div class="nav-menu-submenu-wrap">'
|
|
121
|
+
+ '<button type="button" role="menuitem" aria-haspopup="true" class="nav-menu-parent">'
|
|
122
|
+
+ `${icon}${label}${MENU_CHEVRON_ICON_HTML}</button>`
|
|
123
|
+
+ `<div class="nav-context-submenu" role="menu">${children}</div></div>`;
|
|
124
|
+
}
|
|
125
|
+
const shortcut = item.shortcut
|
|
126
|
+
? `<span class="nav-menu-shortcut">${escapeHtml(item.shortcut)}</span>`
|
|
127
|
+
: '';
|
|
128
|
+
return `<button type="button" role="menuitem" data-menu-action="${escapeHtml(item.action)}">${icon}${label}${shortcut}</button>`;
|
|
129
|
+
}
|
|
130
|
+
function renderContextMenuItems(items) {
|
|
131
|
+
return items
|
|
132
|
+
.map((item) => (item === null ? MENU_SEPARATOR_HTML : renderContextMenuItem(item)))
|
|
133
|
+
.join('');
|
|
134
|
+
}
|
|
53
135
|
const ALL_HEADINGS = [1, 2, 3, 4, 5, 6];
|
|
54
136
|
const ALL_LAYOUT_CONTROLS = {
|
|
55
137
|
source: true,
|
|
@@ -66,6 +148,7 @@ const ALL_FORMATTING_CONTROLS = {
|
|
|
66
148
|
inlineCode: true,
|
|
67
149
|
codeBlock: true,
|
|
68
150
|
blockquote: true,
|
|
151
|
+
lineBreak: true,
|
|
69
152
|
link: true,
|
|
70
153
|
image: true
|
|
71
154
|
};
|
|
@@ -79,6 +162,7 @@ const NO_FORMATTING_CONTROLS = {
|
|
|
79
162
|
inlineCode: false,
|
|
80
163
|
codeBlock: false,
|
|
81
164
|
blockquote: false,
|
|
165
|
+
lineBreak: false,
|
|
82
166
|
link: false,
|
|
83
167
|
image: false
|
|
84
168
|
};
|
|
@@ -228,6 +312,12 @@ function resolveFormattingControls(base, override) {
|
|
|
228
312
|
: [...new Set(headings)].filter((level) => ALL_HEADINGS.includes(level)).sort()
|
|
229
313
|
};
|
|
230
314
|
}
|
|
315
|
+
function normalizeToolbarDensity(value) {
|
|
316
|
+
return value === 'compact' || value === 'dense' ? value : 'comfortable';
|
|
317
|
+
}
|
|
318
|
+
function normalizeContentDensity(value) {
|
|
319
|
+
return value === 'compact' ? value : 'comfortable';
|
|
320
|
+
}
|
|
231
321
|
const mdzipEditorTheme = EditorView.theme({
|
|
232
322
|
'&': {
|
|
233
323
|
height: '100%',
|
|
@@ -242,7 +332,7 @@ const mdzipEditorTheme = EditorView.theme({
|
|
|
242
332
|
overflow: 'auto',
|
|
243
333
|
},
|
|
244
334
|
'.cm-content': {
|
|
245
|
-
padding: '36px 48px',
|
|
335
|
+
padding: 'var(--mdzip-editor-content-padding, var(--mdzip-density-editor-content-padding, 36px 48px))',
|
|
246
336
|
caretColor: 'var(--mdzip-editor-cursor-color)',
|
|
247
337
|
overflowWrap: 'anywhere',
|
|
248
338
|
wordBreak: 'normal',
|
|
@@ -273,6 +363,10 @@ const mdzipEditorTheme = EditorView.theme({
|
|
|
273
363
|
'.cm-activeLineGutter': {
|
|
274
364
|
background: 'transparent',
|
|
275
365
|
},
|
|
366
|
+
'.mdzip-hard-break-marker': {
|
|
367
|
+
color: 'var(--mdzip-muted-foreground-color)',
|
|
368
|
+
opacity: '0.65',
|
|
369
|
+
},
|
|
276
370
|
});
|
|
277
371
|
const mdzipMarkdownHighlight = HighlightStyle.define([
|
|
278
372
|
{ tag: [tags.heading1, tags.heading2, tags.heading3, tags.heading4, tags.heading5, tags.heading6],
|
|
@@ -288,6 +382,22 @@ const mdzipMarkdownHighlight = HighlightStyle.define([
|
|
|
288
382
|
{ tag: tags.contentSeparator, color: '#6a9955' },
|
|
289
383
|
{ tag: tags.atom, color: '#d100d1' },
|
|
290
384
|
]);
|
|
385
|
+
const hardBreakMarkerMatcher = new MatchDecorator({
|
|
386
|
+
regexp: /<br\s*\/?>/gi,
|
|
387
|
+
decoration: Decoration.mark({ class: 'mdzip-hard-break-marker' })
|
|
388
|
+
});
|
|
389
|
+
const hardBreakMarkerHighlight = ViewPlugin.fromClass(class {
|
|
390
|
+
constructor(view) {
|
|
391
|
+
this.decorations = hardBreakMarkerMatcher.createDeco(view);
|
|
392
|
+
}
|
|
393
|
+
update(update) {
|
|
394
|
+
if (update.docChanged || update.viewportChanged) {
|
|
395
|
+
this.decorations = hardBreakMarkerMatcher.updateDeco(update, this.decorations);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}, {
|
|
399
|
+
decorations: value => value.decorations
|
|
400
|
+
});
|
|
291
401
|
function injectStyles(doc) {
|
|
292
402
|
const existing = doc.querySelector(`style[${STYLE_ATTR}]`);
|
|
293
403
|
if (existing) {
|
|
@@ -312,7 +422,27 @@ function attributesToHtml(attrs) {
|
|
|
312
422
|
.map(([key, value]) => ` ${key}="${escapeHtml(String(value))}"`)
|
|
313
423
|
.join('');
|
|
314
424
|
}
|
|
315
|
-
|
|
425
|
+
// Renders the per-row indent/guide cells: one fixed-width cell per ancestor
|
|
426
|
+
// depth. An ancestor column draws a continuous vertical rail when that folder
|
|
427
|
+
// still has siblings below it (so the spine keeps going), otherwise it is
|
|
428
|
+
// blank. The final cell connects to this row with an elbow (└, last child) or a
|
|
429
|
+
// tee (├). Painting per row at the row's own height means the guides never
|
|
430
|
+
// overshoot an expanded last subfolder the way a single container rail did.
|
|
431
|
+
function renderNavGuides(trail, isLast) {
|
|
432
|
+
const cells = trail.map((continues) => `<span class="nav-indent${continues ? ' nav-indent-rail' : ''}"></span>`);
|
|
433
|
+
cells.push(`<span class="nav-indent nav-indent-connector${isLast ? '' : ' nav-indent-continues'}"></span>`);
|
|
434
|
+
return cells.join('');
|
|
435
|
+
}
|
|
436
|
+
function renderNavNode(node, state, options,
|
|
437
|
+
// Per-ancestor flags: whether each ancestor column still has a sibling below
|
|
438
|
+
// it (so its rail continues through this row). One entry per guide column
|
|
439
|
+
// above the immediate connector.
|
|
440
|
+
trail = [],
|
|
441
|
+
// Whether this node is the last among its siblings (elbow vs tee connector).
|
|
442
|
+
isLast = true,
|
|
443
|
+
// Nesting depth; depth 0 (root entries) draws no guides.
|
|
444
|
+
depth = 0) {
|
|
445
|
+
const guides = depth === 0 ? '' : renderNavGuides(trail, isLast);
|
|
316
446
|
if (node.entry) {
|
|
317
447
|
const isCurrent = node.entry.path === state.currentPath;
|
|
318
448
|
const isOrphaned = isOrphanedMdzipAsset(node.entry, state);
|
|
@@ -348,17 +478,20 @@ function renderNavNode(node, state, options) {
|
|
|
348
478
|
const draggable = options.allowDrag && !isManifest;
|
|
349
479
|
return `<button type="button" class="${classes}" title="${title}"
|
|
350
480
|
data-nav-path="${safePath}" data-orphan="${isOrphaned ? 'true' : ''}"${draggable ? ' draggable="true"' : ''}>
|
|
351
|
-
<span class="nav-caret"></span>
|
|
481
|
+
${guides}<span class="nav-caret"></span>
|
|
352
482
|
<span class="nav-file-icon ${iconKind}">${iconHtml}</span>
|
|
353
483
|
${orphanBtnHtml}
|
|
354
484
|
<span class="nav-label">${safeName}</span>
|
|
355
485
|
</button>`;
|
|
356
486
|
}
|
|
357
|
-
const
|
|
487
|
+
const childTrail = depth === 0 ? [] : [...trail, !isLast];
|
|
488
|
+
const children = node.children
|
|
489
|
+
.map((child, index) => renderNavNode(child, state, options, childTrail, index === node.children.length - 1, depth + 1))
|
|
490
|
+
.join('');
|
|
358
491
|
const pending = options.pendingFolders.has(node.path.toLowerCase());
|
|
359
492
|
return `<details class="nav-directory${pending ? ' pending-folder' : ''}" open data-nav-dir="${escapeHtml(node.path)}">
|
|
360
493
|
<summary${pending ? ` title="${escapeHtml(node.path)} — not saved until it contains a file"` : ''}>
|
|
361
|
-
<span class="nav-caret" aria-hidden="true"></span>
|
|
494
|
+
${guides}<span class="nav-caret" aria-hidden="true"></span>
|
|
362
495
|
<span class="nav-folder-icon closed">${FOLDER_CLOSED_ICON_HTML}</span>
|
|
363
496
|
<span class="nav-folder-icon open">${FOLDER_OPEN_ICON_HTML}</span>
|
|
364
497
|
<span class="nav-label">${escapeHtml(node.name)}</span>
|
|
@@ -412,10 +545,16 @@ export class MdzipWorkspaceView {
|
|
|
412
545
|
this.metadataDialogOpen = false;
|
|
413
546
|
this.titleDraft = '';
|
|
414
547
|
this.conversionAction = null;
|
|
548
|
+
this.imageInsertDialogState = null;
|
|
415
549
|
this.navPaneWidth = 280;
|
|
416
550
|
this.splitRatio = 0.5;
|
|
417
551
|
this.resizing = false;
|
|
418
|
-
|
|
552
|
+
// Shared overlay menu state. One menu is open at a time, so a single field
|
|
553
|
+
// drives both the nav-pane file menu and the editor selection menu; `kind`
|
|
554
|
+
// selects which item-builder and action-handler run. The editor variant
|
|
555
|
+
// captures the selection range at open time because clicking a menu item can
|
|
556
|
+
// move focus/selection before the action reads it.
|
|
557
|
+
this.contextMenuState = null;
|
|
419
558
|
this.nameDialogState = null;
|
|
420
559
|
this.deleteDialogState = null;
|
|
421
560
|
this.pendingNewFolders = new Set();
|
|
@@ -434,6 +573,7 @@ export class MdzipWorkspaceView {
|
|
|
434
573
|
this.tooltipHideTimer = null;
|
|
435
574
|
this.cmEditor = null;
|
|
436
575
|
this.readOnlyCompartment = new Compartment();
|
|
576
|
+
this.lineNumbersCompartment = new Compartment();
|
|
437
577
|
this.updatingCm = false;
|
|
438
578
|
this.syncing = false;
|
|
439
579
|
this.markdownExtensions = [];
|
|
@@ -458,6 +598,9 @@ export class MdzipWorkspaceView {
|
|
|
458
598
|
this.options = options;
|
|
459
599
|
this.controlPolicy = resolveMdzipControlPolicy(options.controls);
|
|
460
600
|
this.navigationMode = options.navigationMode ?? 'editor';
|
|
601
|
+
this.imageHydrationAnimation = options.imageHydrationAnimation ?? 'auto';
|
|
602
|
+
this.toolbarDensity = options.toolbarDensity ?? 'comfortable';
|
|
603
|
+
this.contentDensity = options.contentDensity ?? 'comfortable';
|
|
461
604
|
this.layout = options.initialLayout ?? defaultLayoutForPolicy(this.controlPolicy);
|
|
462
605
|
this.colorScheme = options.initialColorScheme
|
|
463
606
|
?? (container.ownerDocument.defaultView?.matchMedia('(prefers-color-scheme: dark)').matches
|
|
@@ -473,6 +616,7 @@ export class MdzipWorkspaceView {
|
|
|
473
616
|
container.innerHTML = SHELL_HTML;
|
|
474
617
|
const q = (sel) => container.querySelector(sel);
|
|
475
618
|
this.elRoot = q('.mdzip-root');
|
|
619
|
+
this.applyDensityClasses();
|
|
476
620
|
this.elDocumentStrip = q('[data-ref="document-strip"]');
|
|
477
621
|
this.elToolbar = q('[data-ref="toolbar"]');
|
|
478
622
|
this.elToolbarLeft = q('.toolbar-left');
|
|
@@ -515,6 +659,14 @@ export class MdzipWorkspaceView {
|
|
|
515
659
|
this.elLibraryList = q('[data-ref="library-list"]');
|
|
516
660
|
this.elConversionDialog = q('[data-ref="conversion-dialog"]');
|
|
517
661
|
this.elConversionConfirmBtn = q('[data-ref="conversion-confirm-btn"]');
|
|
662
|
+
this.elImageInsertDialog = q('[data-ref="image-insert-dialog"]');
|
|
663
|
+
this.elImageInsertModeMarkdown = q('[data-ref="image-insert-mode-markdown"]');
|
|
664
|
+
this.elImageInsertModeHtml = q('[data-ref="image-insert-mode-html"]');
|
|
665
|
+
this.elImageInsertAltInput = q('[data-ref="image-insert-alt"]');
|
|
666
|
+
this.elImageInsertSizeModeSelect = q('[data-ref="image-insert-size-mode"]');
|
|
667
|
+
this.elImageInsertSizeValueInput = q('[data-ref="image-insert-size-value"]');
|
|
668
|
+
this.elImageInsertPositionSelect = q('[data-ref="image-insert-position"]');
|
|
669
|
+
this.elImageInsertConfirmBtn = q('[data-ref="image-insert-confirm-btn"]');
|
|
518
670
|
this.elNavMenu = q('[data-ref="nav-menu"]');
|
|
519
671
|
this.elNameDialog = q('[data-ref="name-dialog"]');
|
|
520
672
|
this.elNameDialogHeading = q('[data-ref="name-dialog-heading"]');
|
|
@@ -731,12 +883,12 @@ export class MdzipWorkspaceView {
|
|
|
731
883
|
if (command === 'insert-image') {
|
|
732
884
|
if (this.workspace?.sourceFormat === 'markdown') {
|
|
733
885
|
this.requestMdzConversion(file
|
|
734
|
-
? { kind: 'image-file', file }
|
|
886
|
+
? { kind: 'image-file', file, source: 'picker' }
|
|
735
887
|
: { kind: 'image-picker' });
|
|
736
888
|
return true;
|
|
737
889
|
}
|
|
738
890
|
if (file) {
|
|
739
|
-
await this.insertImageFile(file);
|
|
891
|
+
await this.insertImageFile(file, 'picker');
|
|
740
892
|
}
|
|
741
893
|
else {
|
|
742
894
|
this.elImageInput.click();
|
|
@@ -746,6 +898,15 @@ export class MdzipWorkspaceView {
|
|
|
746
898
|
this.applyMarkdownFormat(command);
|
|
747
899
|
return true;
|
|
748
900
|
}
|
|
901
|
+
// Backs the editor's formatting keybindings (Mod-b/i/k/e). Returns false when
|
|
902
|
+
// the command can't run (read-only, non-markdown) so the key passes through.
|
|
903
|
+
runFormatShortcut(command) {
|
|
904
|
+
if (!this.canExecuteCommand(command)) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
this.applyMarkdownFormat(command);
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
749
910
|
async convertToMdz() {
|
|
750
911
|
if (!this.workspace || this.workspace.mode === 'read-only') {
|
|
751
912
|
return false;
|
|
@@ -765,6 +926,7 @@ export class MdzipWorkspaceView {
|
|
|
765
926
|
}
|
|
766
927
|
destroy() {
|
|
767
928
|
this.conversionDocumentGeneration += 1;
|
|
929
|
+
this.resolveImageInsertDialog(null);
|
|
768
930
|
try {
|
|
769
931
|
this.unsub?.();
|
|
770
932
|
}
|
|
@@ -811,6 +973,55 @@ export class MdzipWorkspaceView {
|
|
|
811
973
|
this.entryMatchMissKey = null;
|
|
812
974
|
this.render();
|
|
813
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Replaces the view control policy without recreating the workspace or
|
|
978
|
+
* CodeMirror editor. In particular, `lineNumbers` is reconfigured through a
|
|
979
|
+
* CodeMirror compartment so text, selection, focus, scroll, and undo history
|
|
980
|
+
* stay intact.
|
|
981
|
+
*/
|
|
982
|
+
setControls(controls) {
|
|
983
|
+
const next = resolveMdzipControlPolicy(controls);
|
|
984
|
+
const lineNumbersChanged = next.lineNumbers !== this.controlPolicy.lineNumbers;
|
|
985
|
+
this.controlPolicy = next;
|
|
986
|
+
if (lineNumbersChanged && this.cmEditor) {
|
|
987
|
+
this.cmEditor.dispatch({
|
|
988
|
+
effects: this.lineNumbersCompartment.reconfigure(next.lineNumbers ? lineNumbers() : [])
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
const snapshot = this.workspace?.snapshot();
|
|
992
|
+
if (snapshot) {
|
|
993
|
+
this.layout = this.validLayoutForSnapshot(this.layout, snapshot);
|
|
994
|
+
}
|
|
995
|
+
this.render();
|
|
996
|
+
}
|
|
997
|
+
setImageHydrationAnimation(animation) {
|
|
998
|
+
const next = animation ?? 'auto';
|
|
999
|
+
if (next === this.imageHydrationAnimation) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
this.imageHydrationAnimation = next;
|
|
1003
|
+
this.resetPreviewState();
|
|
1004
|
+
this.render();
|
|
1005
|
+
}
|
|
1006
|
+
setDensityOptions(options) {
|
|
1007
|
+
const nextToolbarDensity = normalizeToolbarDensity(options.toolbarDensity);
|
|
1008
|
+
const nextContentDensity = normalizeContentDensity(options.contentDensity);
|
|
1009
|
+
if (nextToolbarDensity === this.toolbarDensity && nextContentDensity === this.contentDensity) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
this.toolbarDensity = nextToolbarDensity;
|
|
1013
|
+
this.contentDensity = nextContentDensity;
|
|
1014
|
+
this.applyDensityClasses();
|
|
1015
|
+
}
|
|
1016
|
+
setImageInsertOptions(options) {
|
|
1017
|
+
this.options.imageInsertMode = options.imageInsertMode;
|
|
1018
|
+
this.options.imageInsertHandler = options.imageInsertHandler;
|
|
1019
|
+
}
|
|
1020
|
+
applyDensityClasses() {
|
|
1021
|
+
this.elRoot.classList.remove('toolbar-density-comfortable', 'toolbar-density-compact', 'toolbar-density-dense', 'content-density-comfortable', 'content-density-compact');
|
|
1022
|
+
this.elRoot.classList.add(`toolbar-density-${this.toolbarDensity}`);
|
|
1023
|
+
this.elRoot.classList.add(`content-density-${this.contentDensity}`);
|
|
1024
|
+
}
|
|
814
1025
|
/**
|
|
815
1026
|
* Tears down all custom rendering state: aborts in-flight renders, destroys
|
|
816
1027
|
* mounted extension and entry renderer handles, and clears memo caches.
|
|
@@ -868,6 +1079,7 @@ export class MdzipWorkspaceView {
|
|
|
868
1079
|
// mounted extension handles.
|
|
869
1080
|
return;
|
|
870
1081
|
}
|
|
1082
|
+
const animateImageHydration = this.shouldAnimateImageHydration(memo, snapshot);
|
|
871
1083
|
this.previewAbort?.abort();
|
|
872
1084
|
this.previewAbort = null;
|
|
873
1085
|
this.destroyPreviewHandles();
|
|
@@ -916,14 +1128,14 @@ export class MdzipWorkspaceView {
|
|
|
916
1128
|
}
|
|
917
1129
|
if (typeof result === 'string') {
|
|
918
1130
|
// Sync fast path: no microtask hop when every pipeline stage is sync.
|
|
919
|
-
this.applyPreviewHtml(result, snapshot, context, generation);
|
|
1131
|
+
this.applyPreviewHtml(result, snapshot, context, generation, animateImageHydration);
|
|
920
1132
|
return;
|
|
921
1133
|
}
|
|
922
1134
|
void result.then((html) => {
|
|
923
1135
|
if (generation !== this.previewGeneration || abort.signal.aborted) {
|
|
924
1136
|
return; // Stale: the selection or content moved on while rendering.
|
|
925
1137
|
}
|
|
926
|
-
this.applyPreviewHtml(html, snapshot, context, generation);
|
|
1138
|
+
this.applyPreviewHtml(html, snapshot, context, generation, animateImageHydration);
|
|
927
1139
|
}).catch((error) => {
|
|
928
1140
|
if (generation !== this.previewGeneration || abort.signal.aborted) {
|
|
929
1141
|
return;
|
|
@@ -933,17 +1145,28 @@ export class MdzipWorkspaceView {
|
|
|
933
1145
|
}
|
|
934
1146
|
});
|
|
935
1147
|
}
|
|
936
|
-
applyPreviewHtml(html, snapshot, context, generation) {
|
|
1148
|
+
applyPreviewHtml(html, snapshot, context, generation, animateImageHydration) {
|
|
937
1149
|
// When the preview references archive images, mount the text immediately
|
|
938
1150
|
// and hydrate each image progressively (reserving layout space from its
|
|
939
1151
|
// sniffed intrinsic size), rather than blocking the whole preview on image
|
|
940
1152
|
// resolution. Other markdown mounts synchronously.
|
|
941
1153
|
if (this.assetSession && /<img\b/i.test(html)) {
|
|
942
|
-
this.mountProgressivePreview(html, snapshot, context, generation);
|
|
1154
|
+
this.mountProgressivePreview(html, snapshot, context, generation, animateImageHydration);
|
|
943
1155
|
return;
|
|
944
1156
|
}
|
|
945
1157
|
this.mountPreviewHtml(html, snapshot, context, generation);
|
|
946
1158
|
}
|
|
1159
|
+
shouldAnimateImageHydration(previousMemo, snapshot) {
|
|
1160
|
+
if (this.imageHydrationAnimation === 'off') {
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
if (this.imageHydrationAnimation === 'auto') {
|
|
1164
|
+
return true;
|
|
1165
|
+
}
|
|
1166
|
+
return !previousMemo
|
|
1167
|
+
|| previousMemo.path !== snapshot.currentPath
|
|
1168
|
+
|| previousMemo.pathType !== snapshot.currentPathType;
|
|
1169
|
+
}
|
|
947
1170
|
mountPreviewHtml(html, snapshot, context, generation) {
|
|
948
1171
|
this.elPreviewContent.innerHTML = html;
|
|
949
1172
|
this.mountPreviewExtensions(context, generation);
|
|
@@ -957,7 +1180,7 @@ export class MdzipWorkspaceView {
|
|
|
957
1180
|
* text is in the DOM; `onAssetsHydrated` fires once every referenced image
|
|
958
1181
|
* has resolved and had its final `src` assigned (or there are none).
|
|
959
1182
|
*/
|
|
960
|
-
mountProgressivePreview(html, snapshot, context, generation) {
|
|
1183
|
+
mountProgressivePreview(html, snapshot, context, generation, animateImageHydration) {
|
|
961
1184
|
this.elPreviewContent.innerHTML = html;
|
|
962
1185
|
const session = this.assetSession;
|
|
963
1186
|
const document = this.elPreviewContent.ownerDocument;
|
|
@@ -973,9 +1196,20 @@ export class MdzipWorkspaceView {
|
|
|
973
1196
|
// immediately readable; the slot eases open to the reserved height once
|
|
974
1197
|
// the image resolves.
|
|
975
1198
|
image.removeAttribute('src');
|
|
976
|
-
|
|
1199
|
+
if (animateImageHydration) {
|
|
1200
|
+
image.classList.add('mdzip-image-loading');
|
|
1201
|
+
}
|
|
977
1202
|
const slot = document.createElement('span');
|
|
978
|
-
slot.className =
|
|
1203
|
+
slot.className = animateImageHydration
|
|
1204
|
+
? 'mdzip-image-slot'
|
|
1205
|
+
: 'mdzip-image-slot mdzip-image-open mdzip-image-animation-off';
|
|
1206
|
+
const align = image.getAttribute('align')?.toLowerCase();
|
|
1207
|
+
if (align === 'left' || image.classList.contains('mdzip-image-wrap-left')) {
|
|
1208
|
+
slot.classList.add('mdzip-image-align-left');
|
|
1209
|
+
}
|
|
1210
|
+
else if (align === 'right' || image.classList.contains('mdzip-image-wrap-right')) {
|
|
1211
|
+
slot.classList.add('mdzip-image-align-right');
|
|
1212
|
+
}
|
|
979
1213
|
image.parentNode?.insertBefore(slot, image);
|
|
980
1214
|
slot.appendChild(image);
|
|
981
1215
|
pending.push({ image, slot, source });
|
|
@@ -1008,7 +1242,8 @@ export class MdzipWorkspaceView {
|
|
|
1008
1242
|
// Size the reserved box from the sniffed dimensions so the slot eases
|
|
1009
1243
|
// open to the image's exact height in a single slide — and the pixels
|
|
1010
1244
|
// drop into an already-correct box with no further reflow.
|
|
1011
|
-
if (resolved.width && resolved.height
|
|
1245
|
+
if (resolved.width && resolved.height
|
|
1246
|
+
&& !image.hasAttribute('width') && !image.hasAttribute('height')) {
|
|
1012
1247
|
image.setAttribute('width', String(resolved.width));
|
|
1013
1248
|
image.setAttribute('height', String(resolved.height));
|
|
1014
1249
|
}
|
|
@@ -1033,6 +1268,10 @@ export class MdzipWorkspaceView {
|
|
|
1033
1268
|
* under `prefers-reduced-motion`.
|
|
1034
1269
|
*/
|
|
1035
1270
|
openImageSlot(slot) {
|
|
1271
|
+
if (this.imageHydrationAnimation === 'off' || slot.classList.contains('mdzip-image-animation-off')) {
|
|
1272
|
+
slot.classList.add('mdzip-image-open');
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1036
1275
|
void slot.offsetHeight;
|
|
1037
1276
|
slot.classList.add('mdzip-image-open');
|
|
1038
1277
|
}
|
|
@@ -1339,11 +1578,17 @@ export class MdzipWorkspaceView {
|
|
|
1339
1578
|
const state = EditorState.create({
|
|
1340
1579
|
doc: initialText,
|
|
1341
1580
|
extensions: [
|
|
1342
|
-
|
|
1581
|
+
this.lineNumbersCompartment.of(this.controlPolicy.lineNumbers ? lineNumbers() : []),
|
|
1343
1582
|
history(),
|
|
1583
|
+
keymap.of([
|
|
1584
|
+
{ key: 'Mod-b', run: () => self.runFormatShortcut('bold') },
|
|
1585
|
+
{ key: 'Mod-i', run: () => self.runFormatShortcut('italic') },
|
|
1586
|
+
{ key: 'Mod-k', run: () => self.runFormatShortcut('link') }
|
|
1587
|
+
]),
|
|
1344
1588
|
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
|
|
1345
1589
|
markdown(),
|
|
1346
1590
|
syntaxHighlighting(mdzipMarkdownHighlight),
|
|
1591
|
+
hardBreakMarkerHighlight,
|
|
1347
1592
|
EditorView.lineWrapping,
|
|
1348
1593
|
dropCursor(),
|
|
1349
1594
|
mdzipEditorTheme,
|
|
@@ -1499,8 +1744,24 @@ export class MdzipWorkspaceView {
|
|
|
1499
1744
|
this.elPreviewBtn.setAttribute('aria-pressed', String(this.layout === 'preview'));
|
|
1500
1745
|
this.elSplitBtn.setAttribute('aria-pressed', String(this.layout === 'split'));
|
|
1501
1746
|
this.elSourceBtn.setAttribute('aria-pressed', String(this.layout === 'source'));
|
|
1502
|
-
|
|
1503
|
-
|
|
1747
|
+
if (snapshot.sourceFormat === 'markdown') {
|
|
1748
|
+
this.elNavBtn.innerHTML = CONVERT_TO_MDZ_ICON_HTML;
|
|
1749
|
+
this.elNavBtn.classList.remove('active');
|
|
1750
|
+
this.elNavBtn.classList.add('convert-mdz-toggle');
|
|
1751
|
+
this.elNavBtn.setAttribute('title', 'Convert to MDZ');
|
|
1752
|
+
this.elNavBtn.setAttribute('aria-label', 'Convert to MDZ');
|
|
1753
|
+
this.elNavBtn.dataset['tooltip'] = 'Convert to MDZ';
|
|
1754
|
+
this.elNavBtn.removeAttribute('aria-pressed');
|
|
1755
|
+
}
|
|
1756
|
+
else {
|
|
1757
|
+
this.elNavBtn.innerHTML = NAV_TOGGLE_ICON_HTML;
|
|
1758
|
+
this.elNavBtn.classList.remove('convert-mdz-toggle');
|
|
1759
|
+
this.elNavBtn.classList.toggle('active', this.navVisible);
|
|
1760
|
+
this.elNavBtn.setAttribute('title', 'Toggle contents');
|
|
1761
|
+
this.elNavBtn.setAttribute('aria-label', 'Toggle contents');
|
|
1762
|
+
this.elNavBtn.dataset['tooltip'] = 'Toggle contents';
|
|
1763
|
+
this.elNavBtn.setAttribute('aria-pressed', String(this.navVisible));
|
|
1764
|
+
}
|
|
1504
1765
|
this.elZoomBtn.classList.toggle('active', this.zoomOpen);
|
|
1505
1766
|
this.elDarkThemeBtn.classList.toggle('active', this.colorScheme === 'dark');
|
|
1506
1767
|
this.elLightThemeBtn.classList.toggle('active', this.colorScheme === 'light');
|
|
@@ -1517,7 +1778,7 @@ export class MdzipWorkspaceView {
|
|
|
1517
1778
|
this.elNavResizer.classList.toggle('hidden', !showNavigationPane);
|
|
1518
1779
|
this.prunePendingFolders(snapshot);
|
|
1519
1780
|
const navTree = snapshot.sourceFormat === 'mdz'
|
|
1520
|
-
? mergePendingFolders(buildMdzipNavTree(snapshot.content.paths), this.pendingNewFolders)
|
|
1781
|
+
? mergePendingFolders(buildMdzipNavTree(snapshot.content.paths, snapshot.content.entryPoint), this.pendingNewFolders)
|
|
1521
1782
|
: [];
|
|
1522
1783
|
const allowOrphanActions = this.controlPolicy.orphanActions && snapshot.mode !== 'read-only';
|
|
1523
1784
|
const allowFileActions = this.allowFileActions(snapshot);
|
|
@@ -1527,7 +1788,9 @@ export class MdzipWorkspaceView {
|
|
|
1527
1788
|
allowDrag: snapshot.mode !== 'read-only' && snapshot.sourceFormat === 'mdz',
|
|
1528
1789
|
pendingFolders: new Set([...this.pendingNewFolders].map((path) => path.toLowerCase()))
|
|
1529
1790
|
};
|
|
1530
|
-
this.elNavTree.innerHTML = navTree
|
|
1791
|
+
this.elNavTree.innerHTML = navTree
|
|
1792
|
+
.map((n, i) => renderNavNode(n, snapshot, navRenderOptions, [], i === navTree.length - 1, 0))
|
|
1793
|
+
.join('');
|
|
1531
1794
|
this.prepareTooltips();
|
|
1532
1795
|
if (this.cmEditor) {
|
|
1533
1796
|
this.updatingCm = true;
|
|
@@ -1564,28 +1827,38 @@ export class MdzipWorkspaceView {
|
|
|
1564
1827
|
this.elTitleSaveBtn.disabled = !valid;
|
|
1565
1828
|
}
|
|
1566
1829
|
this.elConversionDialog.hidden = this.conversionAction === null;
|
|
1830
|
+
this.elImageInsertDialog.hidden = this.imageInsertDialogState === null;
|
|
1831
|
+
if (this.imageInsertDialogState) {
|
|
1832
|
+
this.elImageInsertAltInput.value = this.imageInsertDialogState.request.defaultAltText;
|
|
1833
|
+
this.elImageInsertModeMarkdown.checked = true;
|
|
1834
|
+
this.elImageInsertModeHtml.checked = false;
|
|
1835
|
+
this.elImageInsertSizeModeSelect.value = 'width';
|
|
1836
|
+
this.elImageInsertSizeValueInput.value = this.imageInsertDialogState.request.intrinsicWidth
|
|
1837
|
+
? String(this.imageInsertDialogState.request.intrinsicWidth)
|
|
1838
|
+
: '';
|
|
1839
|
+
this.elImageInsertPositionSelect.value = 'inline';
|
|
1840
|
+
this.updateImageInsertOptionControls();
|
|
1841
|
+
}
|
|
1567
1842
|
this.elMetadataDialog.hidden = !this.metadataDialogOpen;
|
|
1568
|
-
if (this.
|
|
1569
|
-
const items = this.
|
|
1843
|
+
if (this.contextMenuState) {
|
|
1844
|
+
const items = this.contextMenuState.kind === 'nav'
|
|
1845
|
+
? this.navMenuItems(this.contextMenuState.target, snapshot)
|
|
1846
|
+
: this.editorMenuItems(snapshot);
|
|
1570
1847
|
if (items.length === 0) {
|
|
1571
|
-
this.
|
|
1848
|
+
this.contextMenuState = null;
|
|
1572
1849
|
}
|
|
1573
1850
|
else {
|
|
1574
|
-
this.elNavMenu.innerHTML = items
|
|
1575
|
-
.map((item) => item === null
|
|
1576
|
-
? '<div class="nav-menu-separator" role="separator"></div>'
|
|
1577
|
-
: `<button type="button" role="menuitem" data-menu-action="${escapeHtml(item.action)}">${escapeHtml(item.label)}</button>`)
|
|
1578
|
-
.join('');
|
|
1851
|
+
this.elNavMenu.innerHTML = renderContextMenuItems(items);
|
|
1579
1852
|
this.elNavMenu.hidden = false;
|
|
1580
1853
|
const rect = this.elNavMenu.getBoundingClientRect();
|
|
1581
1854
|
const win = this.elRoot.ownerDocument.defaultView ?? window;
|
|
1582
|
-
const x = Math.max(4, Math.min(this.
|
|
1583
|
-
const y = Math.max(4, Math.min(this.
|
|
1855
|
+
const x = Math.max(4, Math.min(this.contextMenuState.x, win.innerWidth - rect.width - 8));
|
|
1856
|
+
const y = Math.max(4, Math.min(this.contextMenuState.y, win.innerHeight - rect.height - 8));
|
|
1584
1857
|
this.elNavMenu.style.left = `${x}px`;
|
|
1585
1858
|
this.elNavMenu.style.top = `${y}px`;
|
|
1586
1859
|
}
|
|
1587
1860
|
}
|
|
1588
|
-
if (!this.
|
|
1861
|
+
if (!this.contextMenuState) {
|
|
1589
1862
|
this.elNavMenu.hidden = true;
|
|
1590
1863
|
}
|
|
1591
1864
|
this.elNameDialog.hidden = this.nameDialogState === null;
|
|
@@ -1638,9 +1911,9 @@ export class MdzipWorkspaceView {
|
|
|
1638
1911
|
const doc = this.elRoot.ownerDocument;
|
|
1639
1912
|
doc.addEventListener('click', () => {
|
|
1640
1913
|
this.closeFormatMenus();
|
|
1641
|
-
if (this.zoomOpen || this.
|
|
1914
|
+
if (this.zoomOpen || this.contextMenuState) {
|
|
1642
1915
|
this.zoomOpen = false;
|
|
1643
|
-
this.
|
|
1916
|
+
this.contextMenuState = null;
|
|
1644
1917
|
this.render();
|
|
1645
1918
|
}
|
|
1646
1919
|
});
|
|
@@ -1648,8 +1921,8 @@ export class MdzipWorkspaceView {
|
|
|
1648
1921
|
if (e.key !== 'Escape') {
|
|
1649
1922
|
return;
|
|
1650
1923
|
}
|
|
1651
|
-
if (this.
|
|
1652
|
-
this.
|
|
1924
|
+
if (this.contextMenuState || this.deleteDialogState || this.nameDialogState) {
|
|
1925
|
+
this.contextMenuState = null;
|
|
1653
1926
|
this.deleteDialogState = null;
|
|
1654
1927
|
this.nameDialogState = null;
|
|
1655
1928
|
this.render();
|
|
@@ -1758,7 +2031,7 @@ export class MdzipWorkspaceView {
|
|
|
1758
2031
|
const file = this.elImageInput.files?.[0];
|
|
1759
2032
|
this.elImageInput.value = '';
|
|
1760
2033
|
if (file) {
|
|
1761
|
-
void this.insertImageFile(file);
|
|
2034
|
+
void this.insertImageFile(file, 'picker');
|
|
1762
2035
|
}
|
|
1763
2036
|
});
|
|
1764
2037
|
this.elEditToolbar.addEventListener('keydown', (event) => {
|
|
@@ -1901,12 +2174,78 @@ export class MdzipWorkspaceView {
|
|
|
1901
2174
|
this.elConversionConfirmBtn.addEventListener('click', () => {
|
|
1902
2175
|
void this.confirmMdzConversion();
|
|
1903
2176
|
});
|
|
2177
|
+
this.elImageInsertDialog.querySelector('[data-action="cancel-image-insert"]')
|
|
2178
|
+
.addEventListener('click', () => {
|
|
2179
|
+
this.resolveImageInsertDialog(null);
|
|
2180
|
+
});
|
|
2181
|
+
this.elImageInsertConfirmBtn.addEventListener('click', () => {
|
|
2182
|
+
this.resolveImageInsertDialog(this.readImageInsertDialogDecision());
|
|
2183
|
+
});
|
|
2184
|
+
this.elImageInsertModeMarkdown.addEventListener('change', () => this.updateImageInsertOptionControls());
|
|
2185
|
+
this.elImageInsertModeHtml.addEventListener('change', () => this.updateImageInsertOptionControls());
|
|
2186
|
+
this.elImageInsertSizeModeSelect.addEventListener('change', () => {
|
|
2187
|
+
this.resetImageInsertSizeValue();
|
|
2188
|
+
this.updateImageInsertOptionControls();
|
|
2189
|
+
});
|
|
1904
2190
|
this.elNavMenu.addEventListener('click', (e) => {
|
|
1905
2191
|
e.stopPropagation();
|
|
1906
2192
|
const item = e.target.closest('[data-menu-action]');
|
|
1907
|
-
if (item) {
|
|
1908
|
-
|
|
2193
|
+
if (!item) {
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
const action = item.dataset['menuAction'] ?? '';
|
|
2197
|
+
if (this.contextMenuState?.kind === 'editor') {
|
|
2198
|
+
void this.handleEditorMenuAction(action);
|
|
2199
|
+
}
|
|
2200
|
+
else {
|
|
2201
|
+
void this.handleNavMenuAction(action);
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
// Flyout submenus open to the right and aligned with their parent by
|
|
2205
|
+
// default; nudge them within the viewport. Runs on hover (when the submenu
|
|
2206
|
+
// is already displayed, so it can be measured): flips left when it would
|
|
2207
|
+
// overflow the right edge, and shifts up when a tall list (e.g. the code
|
|
2208
|
+
// languages) would run off the bottom.
|
|
2209
|
+
this.elNavMenu.addEventListener('pointerover', (e) => {
|
|
2210
|
+
const wrap = e.target.closest('.nav-menu-submenu-wrap');
|
|
2211
|
+
if (!wrap) {
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const submenu = wrap.querySelector('.nav-context-submenu');
|
|
2215
|
+
if (!submenu) {
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
const win = this.elRoot.ownerDocument.defaultView ?? window;
|
|
2219
|
+
const margin = 8;
|
|
2220
|
+
const rect = wrap.getBoundingClientRect();
|
|
2221
|
+
const width = submenu.offsetWidth || SUBMENU_ESTIMATED_WIDTH;
|
|
2222
|
+
wrap.classList.toggle('open-left', rect.right + width > win.innerWidth - margin);
|
|
2223
|
+
// Vertical: clamp the flyout's top so its full height stays on-screen.
|
|
2224
|
+
// `top` is relative to the parent row (CSS default is -5px).
|
|
2225
|
+
const naturalTop = rect.top - 5;
|
|
2226
|
+
const clampedTop = Math.max(margin, Math.min(naturalTop, win.innerHeight - margin - submenu.offsetHeight));
|
|
2227
|
+
submenu.style.top = `${clampedTop - rect.top}px`;
|
|
2228
|
+
});
|
|
2229
|
+
this.elEditorHost.addEventListener('contextmenu', (e) => {
|
|
2230
|
+
const snapshot = this.workspace?.snapshot();
|
|
2231
|
+
if (!snapshot || !this.cmEditor) {
|
|
2232
|
+
return;
|
|
1909
2233
|
}
|
|
2234
|
+
const selection = this.cmEditor.state.selection.main;
|
|
2235
|
+
this.contextMenuState = {
|
|
2236
|
+
kind: 'editor',
|
|
2237
|
+
from: selection.from,
|
|
2238
|
+
to: selection.to,
|
|
2239
|
+
x: e.clientX,
|
|
2240
|
+
y: e.clientY
|
|
2241
|
+
};
|
|
2242
|
+
if (this.editorMenuItems(snapshot).length === 0) {
|
|
2243
|
+
this.contextMenuState = null;
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
e.preventDefault();
|
|
2247
|
+
e.stopPropagation();
|
|
2248
|
+
this.render();
|
|
1910
2249
|
});
|
|
1911
2250
|
this.elNameInput.addEventListener('input', () => {
|
|
1912
2251
|
if (!this.nameDialogState) {
|
|
@@ -2286,7 +2625,7 @@ export class MdzipWorkspaceView {
|
|
|
2286
2625
|
editor.dispatch({ selection: { anchor: target.selectionStart } });
|
|
2287
2626
|
}
|
|
2288
2627
|
if (action.kind === 'image-file') {
|
|
2289
|
-
await this.insertImageFile(action.file);
|
|
2628
|
+
await this.insertImageFile(action.file, action.source ?? 'picker');
|
|
2290
2629
|
return true;
|
|
2291
2630
|
}
|
|
2292
2631
|
this.elImageInput.click();
|
|
@@ -2312,7 +2651,7 @@ export class MdzipWorkspaceView {
|
|
|
2312
2651
|
return;
|
|
2313
2652
|
}
|
|
2314
2653
|
if (action.kind === 'image-file') {
|
|
2315
|
-
await this.insertImageFile(action.file);
|
|
2654
|
+
await this.insertImageFile(action.file, action.source ?? 'picker');
|
|
2316
2655
|
return;
|
|
2317
2656
|
}
|
|
2318
2657
|
this.elImageInput.click();
|
|
@@ -2332,37 +2671,45 @@ export class MdzipWorkspaceView {
|
|
|
2332
2671
|
if (this.workspace?.sourceFormat === 'markdown') {
|
|
2333
2672
|
const extension = extensionForMime(image.mimeType);
|
|
2334
2673
|
const pastedFile = new window.File([new Blob([image.bytes], { type: image.mimeType })], `pasted.${extension}`, { type: image.mimeType });
|
|
2335
|
-
this.requestMdzConversion({ kind: 'image-file', file: pastedFile });
|
|
2674
|
+
this.requestMdzConversion({ kind: 'image-file', file: pastedFile, source: 'paste' });
|
|
2336
2675
|
return;
|
|
2337
2676
|
}
|
|
2338
|
-
await this.insertImageBytes(image.bytes, image.mimeType
|
|
2677
|
+
await this.insertImageBytes(image.bytes, image.mimeType, {
|
|
2678
|
+
fileName: `pasted.${extensionForMime(image.mimeType)}`,
|
|
2679
|
+
source: 'paste'
|
|
2680
|
+
});
|
|
2339
2681
|
}
|
|
2340
2682
|
catch (error) {
|
|
2341
2683
|
this.options.onFailed?.(error);
|
|
2342
2684
|
}
|
|
2343
2685
|
}
|
|
2344
|
-
async insertImageFile(file) {
|
|
2686
|
+
async insertImageFile(file, source = 'picker') {
|
|
2345
2687
|
if (!file.type.startsWith('image/') && !/\.(png|jpe?g|gif|webp|svg)$/i.test(file.name)) {
|
|
2346
2688
|
return;
|
|
2347
2689
|
}
|
|
2348
2690
|
try {
|
|
2349
|
-
await this.insertImageBytes(new Uint8Array(await file.arrayBuffer()), file.type || imageMimeTypeFromFileName(file.name));
|
|
2691
|
+
await this.insertImageBytes(new Uint8Array(await file.arrayBuffer()), file.type || imageMimeTypeFromFileName(file.name), { fileName: file.name, source });
|
|
2350
2692
|
}
|
|
2351
2693
|
catch (error) {
|
|
2352
2694
|
this.options.onFailed?.(error);
|
|
2353
2695
|
}
|
|
2354
2696
|
}
|
|
2355
|
-
async insertImageBytes(bytes, mimeType) {
|
|
2697
|
+
async insertImageBytes(bytes, mimeType, options) {
|
|
2356
2698
|
const editor = this.cmEditor;
|
|
2357
2699
|
if (!editor) {
|
|
2358
2700
|
return;
|
|
2359
2701
|
}
|
|
2360
2702
|
const selection = editor.state.selection.main;
|
|
2703
|
+
const decision = await this.resolveImageInsertDecision(bytes, mimeType, options);
|
|
2704
|
+
if (!decision) {
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2361
2707
|
const result = await this.workspace?.pasteImage({
|
|
2362
2708
|
bytes,
|
|
2363
2709
|
mimeType,
|
|
2364
2710
|
selectionStart: selection.from,
|
|
2365
|
-
selectionEnd: selection.to
|
|
2711
|
+
selectionEnd: selection.to,
|
|
2712
|
+
markdownImage: (markdownPath) => formatImageInsertMarkdown(markdownPath, decision, editor.state.doc.toString(), selection.from, selection.to)
|
|
2366
2713
|
});
|
|
2367
2714
|
if (result && this.cmEditor) {
|
|
2368
2715
|
this.render();
|
|
@@ -2370,6 +2717,405 @@ export class MdzipWorkspaceView {
|
|
|
2370
2717
|
this.cmEditor.focus();
|
|
2371
2718
|
}
|
|
2372
2719
|
}
|
|
2720
|
+
async resolveImageInsertDecision(bytes, mimeType, options) {
|
|
2721
|
+
const size = sniffImageSize(bytes, mimeType);
|
|
2722
|
+
const request = {
|
|
2723
|
+
fileName: options.fileName || `pasted.${extensionForMime(mimeType)}`,
|
|
2724
|
+
mimeType,
|
|
2725
|
+
intrinsicWidth: size?.width,
|
|
2726
|
+
intrinsicHeight: size?.height,
|
|
2727
|
+
defaultAltText: 'Pasted image',
|
|
2728
|
+
source: options.source
|
|
2729
|
+
};
|
|
2730
|
+
const handler = this.options.imageInsertHandler;
|
|
2731
|
+
if (handler) {
|
|
2732
|
+
try {
|
|
2733
|
+
const handled = await handler(request);
|
|
2734
|
+
if (handled !== undefined) {
|
|
2735
|
+
return normalizeImageInsertDecision(handled);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
catch (error) {
|
|
2739
|
+
this.options.onFailed?.(error);
|
|
2740
|
+
return null;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
const mode = this.options.imageInsertMode ?? 'markdown';
|
|
2744
|
+
if (mode === 'ask') {
|
|
2745
|
+
return this.openImageInsertDialog(request);
|
|
2746
|
+
}
|
|
2747
|
+
return normalizeImageInsertDecision({
|
|
2748
|
+
mode: mode === 'html' ? 'html' : 'markdown',
|
|
2749
|
+
altText: request.defaultAltText,
|
|
2750
|
+
width: mode === 'html' ? request.intrinsicWidth : undefined,
|
|
2751
|
+
height: mode === 'html' ? request.intrinsicHeight : undefined,
|
|
2752
|
+
position: 'inline'
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
openImageInsertDialog(request) {
|
|
2756
|
+
return new Promise((resolve) => {
|
|
2757
|
+
this.resolveImageInsertDialog(null);
|
|
2758
|
+
this.imageInsertDialogState = { request, resolve };
|
|
2759
|
+
this.render();
|
|
2760
|
+
requestAnimationFrame(() => this.elImageInsertAltInput.focus());
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2763
|
+
resolveImageInsertDialog(decision) {
|
|
2764
|
+
const state = this.imageInsertDialogState;
|
|
2765
|
+
if (!state) {
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
this.imageInsertDialogState = null;
|
|
2769
|
+
this.render();
|
|
2770
|
+
state.resolve(normalizeImageInsertDecision(decision));
|
|
2771
|
+
}
|
|
2772
|
+
readImageInsertDialogDecision() {
|
|
2773
|
+
const parseDimension = (value) => {
|
|
2774
|
+
const parsed = Number(value);
|
|
2775
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : undefined;
|
|
2776
|
+
};
|
|
2777
|
+
const request = this.imageInsertDialogState?.request;
|
|
2778
|
+
const sizeMode = this.elImageInsertSizeModeSelect.value;
|
|
2779
|
+
const sizeValue = parseDimension(this.elImageInsertSizeValueInput.value);
|
|
2780
|
+
let width;
|
|
2781
|
+
let height;
|
|
2782
|
+
if (this.elImageInsertModeHtml.checked && sizeValue) {
|
|
2783
|
+
if (sizeMode === 'width') {
|
|
2784
|
+
width = sizeValue;
|
|
2785
|
+
}
|
|
2786
|
+
else if (sizeMode === 'height') {
|
|
2787
|
+
height = sizeValue;
|
|
2788
|
+
}
|
|
2789
|
+
else if (sizeMode === 'percent' && request?.intrinsicWidth && request.intrinsicHeight) {
|
|
2790
|
+
width = Math.max(1, Math.round(request.intrinsicWidth * sizeValue / 100));
|
|
2791
|
+
height = Math.max(1, Math.round(request.intrinsicHeight * sizeValue / 100));
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return {
|
|
2795
|
+
mode: this.elImageInsertModeHtml.checked ? 'html' : 'markdown',
|
|
2796
|
+
altText: this.elImageInsertAltInput.value,
|
|
2797
|
+
width,
|
|
2798
|
+
height,
|
|
2799
|
+
position: this.elImageInsertPositionSelect.value
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
updateImageInsertOptionControls() {
|
|
2803
|
+
const htmlMode = this.elImageInsertModeHtml.checked;
|
|
2804
|
+
const sizeDisabled = !htmlMode || this.elImageInsertSizeModeSelect.value === 'original';
|
|
2805
|
+
this.elImageInsertSizeModeSelect.disabled = !htmlMode;
|
|
2806
|
+
this.elImageInsertSizeValueInput.disabled = sizeDisabled;
|
|
2807
|
+
this.elImageInsertPositionSelect.disabled = !htmlMode;
|
|
2808
|
+
this.elImageInsertSizeModeSelect.closest('.image-insert-field')?.classList.toggle('field-disabled', !htmlMode);
|
|
2809
|
+
this.elImageInsertSizeValueInput.closest('.image-insert-field')?.classList.toggle('field-disabled', sizeDisabled);
|
|
2810
|
+
this.elImageInsertPositionSelect.closest('.image-insert-field')?.classList.toggle('field-disabled', !htmlMode);
|
|
2811
|
+
}
|
|
2812
|
+
resetImageInsertSizeValue() {
|
|
2813
|
+
const request = this.imageInsertDialogState?.request;
|
|
2814
|
+
switch (this.elImageInsertSizeModeSelect.value) {
|
|
2815
|
+
case 'width':
|
|
2816
|
+
this.elImageInsertSizeValueInput.value = request?.intrinsicWidth ? String(request.intrinsicWidth) : '';
|
|
2817
|
+
break;
|
|
2818
|
+
case 'height':
|
|
2819
|
+
this.elImageInsertSizeValueInput.value = request?.intrinsicHeight ? String(request.intrinsicHeight) : '';
|
|
2820
|
+
break;
|
|
2821
|
+
case 'percent':
|
|
2822
|
+
this.elImageInsertSizeValueInput.value = '100';
|
|
2823
|
+
break;
|
|
2824
|
+
default:
|
|
2825
|
+
this.elImageInsertSizeValueInput.value = '';
|
|
2826
|
+
break;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
// Items for the editor selection menu; null entries render as separators.
|
|
2830
|
+
// Reads the selection range captured when the menu opened (not the live
|
|
2831
|
+
// selection) so the displayed actions match what the handlers will act on.
|
|
2832
|
+
editorMenuItems(snapshot) {
|
|
2833
|
+
const state = this.contextMenuState;
|
|
2834
|
+
if (!this.cmEditor || state?.kind !== 'editor') {
|
|
2835
|
+
return [];
|
|
2836
|
+
}
|
|
2837
|
+
const hasSelection = state.to > state.from;
|
|
2838
|
+
const editable = snapshot.mode !== 'read-only' && snapshot.currentPathType === 'markdown';
|
|
2839
|
+
const formatting = this.controlPolicy.formatting;
|
|
2840
|
+
const groups = [];
|
|
2841
|
+
const clipboard = [];
|
|
2842
|
+
if (hasSelection) {
|
|
2843
|
+
if (editable) {
|
|
2844
|
+
clipboard.push({ action: 'editor-cut', label: 'Cut', icon: MENU_CUT_ICON_HTML, shortcut: this.editorShortcut('X') });
|
|
2845
|
+
}
|
|
2846
|
+
clipboard.push({ action: 'editor-copy', label: 'Copy', icon: MENU_COPY_ICON_HTML, shortcut: this.editorShortcut('C') });
|
|
2847
|
+
}
|
|
2848
|
+
if (editable) {
|
|
2849
|
+
clipboard.push({ action: 'editor-paste', label: 'Paste', icon: MENU_PASTE_ICON_HTML, shortcut: this.editorShortcut('V') });
|
|
2850
|
+
clipboard.push({ action: 'editor-paste-plain', label: 'Paste as Plain Text', icon: MENU_PASTE_PLAIN_ICON_HTML });
|
|
2851
|
+
}
|
|
2852
|
+
if (clipboard.length > 0) {
|
|
2853
|
+
groups.push(clipboard);
|
|
2854
|
+
}
|
|
2855
|
+
// Formatting mirrors the toolbar's capabilities so the menu can stand in
|
|
2856
|
+
// for it. Inline marks and block commands apply to the current line when
|
|
2857
|
+
// there's no selection (same as the toolbar), so only Clear Formatting —
|
|
2858
|
+
// which acts on a range — is gated on a selection.
|
|
2859
|
+
const anyInline = formatting.bold || formatting.italic || formatting.strikethrough || formatting.inlineCode;
|
|
2860
|
+
const anyBlock = formatting.headings.length > 0 || formatting.bulletList
|
|
2861
|
+
|| formatting.orderedList || formatting.blockquote || formatting.codeBlock || formatting.lineBreak;
|
|
2862
|
+
if (editable && anyInline) {
|
|
2863
|
+
const inline = [];
|
|
2864
|
+
if (formatting.bold) {
|
|
2865
|
+
inline.push({ action: 'bold', label: 'Bold', icon: MENU_BOLD_ICON_HTML, shortcut: this.editorShortcut('B') });
|
|
2866
|
+
}
|
|
2867
|
+
if (formatting.italic) {
|
|
2868
|
+
inline.push({ action: 'italic', label: 'Italic', icon: MENU_ITALIC_ICON_HTML, shortcut: this.editorShortcut('I') });
|
|
2869
|
+
}
|
|
2870
|
+
if (formatting.strikethrough) {
|
|
2871
|
+
inline.push({ action: 'strikethrough', label: 'Strikethrough', icon: MENU_STRIKE_ICON_HTML });
|
|
2872
|
+
}
|
|
2873
|
+
inline.push({ action: 'highlight', label: 'Highlight', icon: MENU_HIGHLIGHT_ICON_HTML });
|
|
2874
|
+
if (formatting.inlineCode) {
|
|
2875
|
+
inline.push({ action: 'inline-code', label: 'Inline Code', icon: MENU_CODE_ICON_HTML });
|
|
2876
|
+
}
|
|
2877
|
+
groups.push(inline);
|
|
2878
|
+
}
|
|
2879
|
+
if (editable && anyBlock) {
|
|
2880
|
+
const block = [];
|
|
2881
|
+
if (formatting.headings.length > 0) {
|
|
2882
|
+
block.push({
|
|
2883
|
+
action: '',
|
|
2884
|
+
label: 'Heading',
|
|
2885
|
+
icon: MENU_HEADING_PARENT_ICON_HTML,
|
|
2886
|
+
submenu: [
|
|
2887
|
+
{ action: 'paragraph', label: 'Paragraph', icon: MENU_PARAGRAPH_ICON_HTML },
|
|
2888
|
+
null,
|
|
2889
|
+
...formatting.headings.map((level) => ({
|
|
2890
|
+
action: `heading-${level}`,
|
|
2891
|
+
label: `Heading ${level}`,
|
|
2892
|
+
icon: MENU_HEADING_ICON_HTML[level]
|
|
2893
|
+
}))
|
|
2894
|
+
]
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
if (formatting.bulletList) {
|
|
2898
|
+
block.push({ action: 'bullet-list', label: 'Bullet List', icon: MENU_BULLET_LIST_ICON_HTML });
|
|
2899
|
+
}
|
|
2900
|
+
if (formatting.orderedList) {
|
|
2901
|
+
block.push({ action: 'ordered-list', label: 'Numbered List', icon: MENU_ORDERED_LIST_ICON_HTML });
|
|
2902
|
+
}
|
|
2903
|
+
if (formatting.blockquote) {
|
|
2904
|
+
block.push({ action: 'blockquote', label: 'Blockquote', icon: MENU_QUOTE_ICON_HTML });
|
|
2905
|
+
}
|
|
2906
|
+
if (formatting.codeBlock) {
|
|
2907
|
+
const languages = this.options.codeBlockLanguages ?? DEFAULT_CODE_BLOCK_LANGUAGES;
|
|
2908
|
+
block.push({
|
|
2909
|
+
action: '',
|
|
2910
|
+
label: 'Code Block',
|
|
2911
|
+
icon: MENU_CODE_BLOCK_ICON_HTML,
|
|
2912
|
+
submenu: languages.map((lang) => ({
|
|
2913
|
+
action: `code-block:${lang.id}`,
|
|
2914
|
+
label: lang.label
|
|
2915
|
+
}))
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
if (formatting.lineBreak) {
|
|
2919
|
+
block.push({ action: 'insert-line-break', label: 'Line Break', icon: MENU_LINE_BREAK_ICON_HTML });
|
|
2920
|
+
}
|
|
2921
|
+
groups.push(block);
|
|
2922
|
+
}
|
|
2923
|
+
if (editable && (formatting.link || formatting.image)) {
|
|
2924
|
+
const insert = [];
|
|
2925
|
+
if (formatting.link) {
|
|
2926
|
+
insert.push({ action: 'link', label: 'Link…', icon: MENU_LINK_ICON_HTML, shortcut: this.editorShortcut('K') });
|
|
2927
|
+
}
|
|
2928
|
+
if (formatting.image) {
|
|
2929
|
+
insert.push({ action: 'insert-image', label: 'Insert Image…', icon: MENU_IMAGE_ICON_HTML });
|
|
2930
|
+
}
|
|
2931
|
+
groups.push(insert);
|
|
2932
|
+
}
|
|
2933
|
+
if (editable && hasSelection && (anyInline || anyBlock)) {
|
|
2934
|
+
groups.push([{ action: 'editor-clear-format', label: 'Clear Formatting', icon: MENU_CLEAR_FORMAT_ICON_HTML }]);
|
|
2935
|
+
}
|
|
2936
|
+
groups.push([{ action: 'editor-select-all', label: 'Select All', icon: MENU_SELECT_ALL_ICON_HTML, shortcut: this.editorShortcut('A') }]);
|
|
2937
|
+
return groups.flatMap((group, index) => (index === 0 ? group : [null, ...group]));
|
|
2938
|
+
}
|
|
2939
|
+
async handleEditorMenuAction(action) {
|
|
2940
|
+
const state = this.contextMenuState;
|
|
2941
|
+
this.contextMenuState = null;
|
|
2942
|
+
this.render();
|
|
2943
|
+
const editor = this.cmEditor;
|
|
2944
|
+
if (!editor || state?.kind !== 'editor') {
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
// Clicking the menu may have moved focus and collapsed the selection, so
|
|
2948
|
+
// restore the range captured when the menu opened before acting on it.
|
|
2949
|
+
if (action !== 'editor-select-all') {
|
|
2950
|
+
editor.dispatch({ selection: { anchor: state.from, head: state.to } });
|
|
2951
|
+
}
|
|
2952
|
+
// Code Block submenu items carry the chosen language as a suffix.
|
|
2953
|
+
if (action.startsWith('code-block:')) {
|
|
2954
|
+
this.insertCodeBlock(action.slice('code-block:'.length));
|
|
2955
|
+
editor.focus();
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
switch (action) {
|
|
2959
|
+
case 'editor-cut':
|
|
2960
|
+
await this.cutEditorSelection();
|
|
2961
|
+
break;
|
|
2962
|
+
case 'editor-copy':
|
|
2963
|
+
await this.copyEditorSelection();
|
|
2964
|
+
break;
|
|
2965
|
+
case 'editor-paste':
|
|
2966
|
+
case 'editor-paste-plain':
|
|
2967
|
+
await this.pasteIntoEditor();
|
|
2968
|
+
break;
|
|
2969
|
+
case 'editor-clear-format':
|
|
2970
|
+
this.clearSelectionFormatting();
|
|
2971
|
+
editor.focus();
|
|
2972
|
+
break;
|
|
2973
|
+
case 'editor-select-all':
|
|
2974
|
+
editor.dispatch({ selection: { anchor: 0, head: editor.state.doc.length } });
|
|
2975
|
+
editor.focus();
|
|
2976
|
+
break;
|
|
2977
|
+
case 'insert-image':
|
|
2978
|
+
await this.executeCommand('insert-image');
|
|
2979
|
+
break;
|
|
2980
|
+
case 'bold':
|
|
2981
|
+
case 'italic':
|
|
2982
|
+
case 'strikethrough':
|
|
2983
|
+
case 'highlight':
|
|
2984
|
+
case 'inline-code':
|
|
2985
|
+
case 'blockquote':
|
|
2986
|
+
case 'bullet-list':
|
|
2987
|
+
case 'ordered-list':
|
|
2988
|
+
case 'insert-line-break':
|
|
2989
|
+
case 'link':
|
|
2990
|
+
case 'paragraph':
|
|
2991
|
+
case 'heading-1':
|
|
2992
|
+
case 'heading-2':
|
|
2993
|
+
case 'heading-3':
|
|
2994
|
+
case 'heading-4':
|
|
2995
|
+
case 'heading-5':
|
|
2996
|
+
case 'heading-6':
|
|
2997
|
+
this.applyMarkdownFormat(action);
|
|
2998
|
+
editor.focus();
|
|
2999
|
+
break;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
// Inserts a fenced code block, carrying the chosen language as the fence info
|
|
3003
|
+
// string. An empty language produces a plain ```` ``` ```` block.
|
|
3004
|
+
insertCodeBlock(language) {
|
|
3005
|
+
if (!this.canExecuteCommand('code-block')) {
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
this.wrapSelection(`\`\`\`${language}\n`, '\n```', 'code');
|
|
3009
|
+
}
|
|
3010
|
+
// Strips common Markdown formatting from the selection: leading block markers
|
|
3011
|
+
// (headings, blockquotes, list bullets) per line, then inline emphasis,
|
|
3012
|
+
// highlight, and code spans. Heuristic by nature — it targets the markers the
|
|
3013
|
+
// editor itself produces rather than parsing the full grammar.
|
|
3014
|
+
clearSelectionFormatting() {
|
|
3015
|
+
const editor = this.cmEditor;
|
|
3016
|
+
const snapshot = this.workspace?.snapshot();
|
|
3017
|
+
if (!editor || !snapshot || snapshot.mode === 'read-only') {
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
const selection = editor.state.selection.main;
|
|
3021
|
+
if (selection.empty) {
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
const cleared = editor.state.sliceDoc(selection.from, selection.to)
|
|
3025
|
+
.split('\n')
|
|
3026
|
+
.map((line) => line
|
|
3027
|
+
.replace(/^(\s*)#{1,6}\s+/, '$1')
|
|
3028
|
+
.replace(/^(\s*)>\s?/, '$1')
|
|
3029
|
+
.replace(/^(\s*)(?:[-*+]|\d+\.)\s+/, '$1'))
|
|
3030
|
+
.join('\n')
|
|
3031
|
+
.replace(/<\/?mark>/gi, '')
|
|
3032
|
+
.replace(/(\*\*|__)(.*?)\1/g, '$2')
|
|
3033
|
+
.replace(/(\*|_)(.*?)\1/g, '$2')
|
|
3034
|
+
.replace(/~~(.*?)~~/g, '$1')
|
|
3035
|
+
.replace(/==(.*?)==/g, '$1')
|
|
3036
|
+
.replace(/`([^`]*)`/g, '$1');
|
|
3037
|
+
editor.dispatch({
|
|
3038
|
+
changes: { from: selection.from, to: selection.to, insert: cleared },
|
|
3039
|
+
selection: { anchor: selection.from, head: selection.from + cleared.length }
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
editorClipboard() {
|
|
3043
|
+
return this.elRoot.ownerDocument.defaultView?.navigator?.clipboard;
|
|
3044
|
+
}
|
|
3045
|
+
// Renders a single-letter shortcut for the host platform: ⌘X on macOS,
|
|
3046
|
+
// Ctrl+X elsewhere. Only used for bindings that genuinely fire (the native
|
|
3047
|
+
// clipboard keys and `defaultKeymap`'s select-all).
|
|
3048
|
+
editorShortcut(key) {
|
|
3049
|
+
const nav = this.elRoot.ownerDocument.defaultView?.navigator;
|
|
3050
|
+
const platform = nav?.userAgentData?.platform
|
|
3051
|
+
?? nav?.platform
|
|
3052
|
+
?? '';
|
|
3053
|
+
return /mac|iphone|ipad|ipod/i.test(platform) ? `⌘${key}` : `Ctrl+${key}`;
|
|
3054
|
+
}
|
|
3055
|
+
async copyEditorSelection() {
|
|
3056
|
+
const editor = this.cmEditor;
|
|
3057
|
+
if (!editor) {
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
const selection = editor.state.selection.main;
|
|
3061
|
+
if (selection.empty) {
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
try {
|
|
3065
|
+
await this.editorClipboard()?.writeText(editor.state.sliceDoc(selection.from, selection.to));
|
|
3066
|
+
}
|
|
3067
|
+
catch (error) {
|
|
3068
|
+
this.options.onFailed?.(error);
|
|
3069
|
+
}
|
|
3070
|
+
editor.focus();
|
|
3071
|
+
}
|
|
3072
|
+
async cutEditorSelection() {
|
|
3073
|
+
const editor = this.cmEditor;
|
|
3074
|
+
const snapshot = this.workspace?.snapshot();
|
|
3075
|
+
if (!editor || !snapshot || snapshot.mode === 'read-only') {
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
const selection = editor.state.selection.main;
|
|
3079
|
+
if (selection.empty) {
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
try {
|
|
3083
|
+
await this.editorClipboard()?.writeText(editor.state.sliceDoc(selection.from, selection.to));
|
|
3084
|
+
}
|
|
3085
|
+
catch (error) {
|
|
3086
|
+
this.options.onFailed?.(error);
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
editor.dispatch({
|
|
3090
|
+
changes: { from: selection.from, to: selection.to, insert: '' },
|
|
3091
|
+
selection: { anchor: selection.from }
|
|
3092
|
+
});
|
|
3093
|
+
editor.focus();
|
|
3094
|
+
}
|
|
3095
|
+
async pasteIntoEditor() {
|
|
3096
|
+
const editor = this.cmEditor;
|
|
3097
|
+
const snapshot = this.workspace?.snapshot();
|
|
3098
|
+
if (!editor || !snapshot || snapshot.mode === 'read-only') {
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
let text;
|
|
3102
|
+
try {
|
|
3103
|
+
text = await this.editorClipboard()?.readText();
|
|
3104
|
+
}
|
|
3105
|
+
catch (error) {
|
|
3106
|
+
this.options.onFailed?.(error);
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
if (!text) {
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
const selection = editor.state.selection.main;
|
|
3113
|
+
editor.dispatch({
|
|
3114
|
+
changes: { from: selection.from, to: selection.to, insert: text },
|
|
3115
|
+
selection: { anchor: selection.from + text.length }
|
|
3116
|
+
});
|
|
3117
|
+
editor.focus();
|
|
3118
|
+
}
|
|
2373
3119
|
applyMarkdownFormat(format) {
|
|
2374
3120
|
const editor = this.cmEditor;
|
|
2375
3121
|
const snapshot = this.workspace?.snapshot();
|
|
@@ -2386,6 +3132,9 @@ export class MdzipWorkspaceView {
|
|
|
2386
3132
|
case 'strikethrough':
|
|
2387
3133
|
this.wrapSelection('~~', '~~', 'strikethrough text');
|
|
2388
3134
|
break;
|
|
3135
|
+
case 'highlight':
|
|
3136
|
+
this.wrapSelection('<mark>', '</mark>', 'highlighted text');
|
|
3137
|
+
break;
|
|
2389
3138
|
case 'paragraph':
|
|
2390
3139
|
this.setSelectedLinePrefix('', /^(#{1,6})\s+/);
|
|
2391
3140
|
break;
|
|
@@ -2412,8 +3161,11 @@ export class MdzipWorkspaceView {
|
|
|
2412
3161
|
case 'blockquote':
|
|
2413
3162
|
this.prefixSelectedLines('> ', /^(\s*)>\s?/);
|
|
2414
3163
|
break;
|
|
3164
|
+
case 'insert-line-break':
|
|
3165
|
+
this.insertLineBreak();
|
|
3166
|
+
break;
|
|
2415
3167
|
case 'link':
|
|
2416
|
-
this.
|
|
3168
|
+
this.insertMarkdownLink();
|
|
2417
3169
|
break;
|
|
2418
3170
|
default:
|
|
2419
3171
|
return;
|
|
@@ -2430,6 +3182,7 @@ export class MdzipWorkspaceView {
|
|
|
2430
3182
|
orderedList: formatting.orderedList,
|
|
2431
3183
|
code: formatting.inlineCode || formatting.codeBlock,
|
|
2432
3184
|
blockquote: formatting.blockquote,
|
|
3185
|
+
lineBreak: formatting.lineBreak,
|
|
2433
3186
|
link: formatting.link,
|
|
2434
3187
|
image: formatting.image
|
|
2435
3188
|
};
|
|
@@ -2490,6 +3243,38 @@ export class MdzipWorkspaceView {
|
|
|
2490
3243
|
});
|
|
2491
3244
|
editor.focus();
|
|
2492
3245
|
}
|
|
3246
|
+
insertMarkdownLink() {
|
|
3247
|
+
const editor = this.cmEditor;
|
|
3248
|
+
if (!editor) {
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
const selection = editor.state.selection.main;
|
|
3252
|
+
const selectedText = editor.state.sliceDoc(selection.from, selection.to);
|
|
3253
|
+
const linkText = selectedText || 'link text';
|
|
3254
|
+
const urlPlaceholder = 'url';
|
|
3255
|
+
const insert = `[${linkText}](${urlPlaceholder})`;
|
|
3256
|
+
const anchor = selection.from + linkText.length + 3;
|
|
3257
|
+
editor.dispatch({
|
|
3258
|
+
changes: { from: selection.from, to: selection.to, insert },
|
|
3259
|
+
selection: { anchor, head: anchor + urlPlaceholder.length },
|
|
3260
|
+
scrollIntoView: true
|
|
3261
|
+
});
|
|
3262
|
+
editor.focus();
|
|
3263
|
+
}
|
|
3264
|
+
insertLineBreak() {
|
|
3265
|
+
const editor = this.cmEditor;
|
|
3266
|
+
if (!editor) {
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
const selection = editor.state.selection.main;
|
|
3270
|
+
const insert = '<br>\n';
|
|
3271
|
+
editor.dispatch({
|
|
3272
|
+
changes: { from: selection.from, to: selection.to, insert },
|
|
3273
|
+
selection: { anchor: selection.from + insert.length },
|
|
3274
|
+
scrollIntoView: true
|
|
3275
|
+
});
|
|
3276
|
+
editor.focus();
|
|
3277
|
+
}
|
|
2493
3278
|
prefixSelectedLines(prefix, existingPrefix) {
|
|
2494
3279
|
const editor = this.cmEditor;
|
|
2495
3280
|
if (!editor) {
|
|
@@ -2688,7 +3473,7 @@ export class MdzipWorkspaceView {
|
|
|
2688
3473
|
const bounds = event.target?.getBoundingClientRect();
|
|
2689
3474
|
const clientX = event instanceof MouseEvent ? event.clientX : (bounds?.left ?? 0);
|
|
2690
3475
|
const clientY = event instanceof MouseEvent ? event.clientY : (bounds?.bottom ?? 0);
|
|
2691
|
-
this.
|
|
3476
|
+
this.contextMenuState = { kind: 'nav', target, x: clientX, y: clientY };
|
|
2692
3477
|
this.render();
|
|
2693
3478
|
}
|
|
2694
3479
|
// Items for the nav context menu; null entries render as separators.
|
|
@@ -2746,10 +3531,10 @@ export class MdzipWorkspaceView {
|
|
|
2746
3531
|
return groups.flatMap((group, index) => index === 0 ? group : [null, ...group]);
|
|
2747
3532
|
}
|
|
2748
3533
|
async handleNavMenuAction(action) {
|
|
2749
|
-
const state = this.
|
|
2750
|
-
this.
|
|
3534
|
+
const state = this.contextMenuState;
|
|
3535
|
+
this.contextMenuState = null;
|
|
2751
3536
|
this.render();
|
|
2752
|
-
if (!state) {
|
|
3537
|
+
if (!state || state.kind !== 'nav') {
|
|
2753
3538
|
return;
|
|
2754
3539
|
}
|
|
2755
3540
|
const target = state.target;
|
|
@@ -2985,7 +3770,7 @@ export class MdzipWorkspaceView {
|
|
|
2985
3770
|
async handleEditorImageDrop(file, x, y) {
|
|
2986
3771
|
try {
|
|
2987
3772
|
if (this.workspace?.sourceFormat === 'markdown') {
|
|
2988
|
-
this.requestMdzConversion({ kind: 'image-file', file });
|
|
3773
|
+
this.requestMdzConversion({ kind: 'image-file', file, source: 'drop' });
|
|
2989
3774
|
return;
|
|
2990
3775
|
}
|
|
2991
3776
|
const editor = this.cmEditor;
|
|
@@ -2995,7 +3780,7 @@ export class MdzipWorkspaceView {
|
|
|
2995
3780
|
editor.dispatch({ selection: { anchor: pos } });
|
|
2996
3781
|
}
|
|
2997
3782
|
}
|
|
2998
|
-
await this.insertImageFile(file);
|
|
3783
|
+
await this.insertImageFile(file, 'drop');
|
|
2999
3784
|
}
|
|
3000
3785
|
catch (error) {
|
|
3001
3786
|
this.options.onFailed?.(error);
|
|
@@ -3209,6 +3994,7 @@ function hasFormattingControls(policy) {
|
|
|
3209
3994
|
|| policy.inlineCode
|
|
3210
3995
|
|| policy.codeBlock
|
|
3211
3996
|
|| policy.blockquote
|
|
3997
|
+
|| policy.lineBreak
|
|
3212
3998
|
|| policy.link
|
|
3213
3999
|
|| policy.image;
|
|
3214
4000
|
}
|
|
@@ -3228,6 +4014,8 @@ function markdownCommandFromToolbarFormat(format) {
|
|
|
3228
4014
|
case 'code-block':
|
|
3229
4015
|
case 'link':
|
|
3230
4016
|
return format;
|
|
4017
|
+
case 'line-break':
|
|
4018
|
+
return 'insert-line-break';
|
|
3231
4019
|
case 'strike':
|
|
3232
4020
|
return 'strikethrough';
|
|
3233
4021
|
case 'code':
|
|
@@ -3255,6 +4043,70 @@ function imageMimeTypeFromFileName(fileName) {
|
|
|
3255
4043
|
return 'image/png';
|
|
3256
4044
|
}
|
|
3257
4045
|
}
|
|
4046
|
+
function normalizeImageInsertDecision(decision) {
|
|
4047
|
+
if (!decision) {
|
|
4048
|
+
return null;
|
|
4049
|
+
}
|
|
4050
|
+
const normalizeDimension = (value) => value !== undefined && Number.isFinite(value) && value > 0
|
|
4051
|
+
? Math.round(value)
|
|
4052
|
+
: undefined;
|
|
4053
|
+
return {
|
|
4054
|
+
mode: decision.mode === 'html' ? 'html' : 'markdown',
|
|
4055
|
+
altText: decision.altText ?? '',
|
|
4056
|
+
width: normalizeDimension(decision.width),
|
|
4057
|
+
height: normalizeDimension(decision.height),
|
|
4058
|
+
position: normalizeImagePosition(decision.position)
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
function normalizeImagePosition(position) {
|
|
4062
|
+
switch (position) {
|
|
4063
|
+
case 'left':
|
|
4064
|
+
case 'center':
|
|
4065
|
+
case 'right':
|
|
4066
|
+
case 'wrap-left':
|
|
4067
|
+
case 'wrap-right':
|
|
4068
|
+
return position;
|
|
4069
|
+
default:
|
|
4070
|
+
return 'inline';
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
function formatImageInsertMarkdown(markdownPath, decision, currentText = '', selectionStart = 0, selectionEnd = selectionStart) {
|
|
4074
|
+
if (decision.mode === 'html') {
|
|
4075
|
+
const attrs = [
|
|
4076
|
+
`src="${escapeHtml(markdownPath)}"`,
|
|
4077
|
+
`alt="${escapeHtml(decision.altText)}"`
|
|
4078
|
+
];
|
|
4079
|
+
if (decision.width) {
|
|
4080
|
+
attrs.push(`width="${decision.width}"`);
|
|
4081
|
+
}
|
|
4082
|
+
if (decision.height) {
|
|
4083
|
+
attrs.push(`height="${decision.height}"`);
|
|
4084
|
+
}
|
|
4085
|
+
if (decision.position === 'wrap-left' || decision.position === 'wrap-right') {
|
|
4086
|
+
attrs.push(`align="${decision.position === 'wrap-left' ? 'left' : 'right'}"`);
|
|
4087
|
+
}
|
|
4088
|
+
const image = `<img ${attrs.join(' ')}>`;
|
|
4089
|
+
if (decision.position === 'left' || decision.position === 'center' || decision.position === 'right') {
|
|
4090
|
+
return padHtmlImageBlock(`<p align="${decision.position}">${image}</p>`, currentText, selectionStart, selectionEnd);
|
|
4091
|
+
}
|
|
4092
|
+
return padHtmlImageBlock(image, currentText, selectionStart, selectionEnd);
|
|
4093
|
+
}
|
|
4094
|
+
return ``;
|
|
4095
|
+
}
|
|
4096
|
+
function padHtmlImageBlock(html, currentText, selectionStart, selectionEnd) {
|
|
4097
|
+
const before = currentText.slice(0, Math.max(0, selectionStart));
|
|
4098
|
+
const after = currentText.slice(Math.max(selectionStart, selectionEnd));
|
|
4099
|
+
const prefix = before.length === 0 || before.endsWith('\n\n')
|
|
4100
|
+
? ''
|
|
4101
|
+
: before.endsWith('\n') ? '\n' : '\n\n';
|
|
4102
|
+
const suffix = after.startsWith('\n\n')
|
|
4103
|
+
? ''
|
|
4104
|
+
: after.startsWith('\n') ? '\n' : '\n\n';
|
|
4105
|
+
return `${prefix}${html}${suffix}`;
|
|
4106
|
+
}
|
|
4107
|
+
function escapeMarkdownImageAlt(value) {
|
|
4108
|
+
return value.replace(/\\/g, '\\\\').replace(/]/g, '\\]');
|
|
4109
|
+
}
|
|
3258
4110
|
function formatMetadataValue(value) {
|
|
3259
4111
|
if (typeof value === 'string' && value.trim()) {
|
|
3260
4112
|
return value;
|
|
@@ -3319,6 +4171,7 @@ const SHELL_HTML = `
|
|
|
3319
4171
|
</div>
|
|
3320
4172
|
</div>
|
|
3321
4173
|
<button type="button" data-format="quote" data-format-control="blockquote" title="Blockquote" aria-label="Blockquote">${QUOTE_ICON_HTML}</button>
|
|
4174
|
+
<button type="button" data-format="line-break" data-format-control="lineBreak" title="Line break" aria-label="Line break">${LINE_BREAK_ICON_HTML}</button>
|
|
3322
4175
|
</div>
|
|
3323
4176
|
<span class="edit-toolbar-divider" aria-hidden="true"></span>
|
|
3324
4177
|
<div class="edit-toolbar-group">
|
|
@@ -3425,6 +4278,61 @@ const SHELL_HTML = `
|
|
|
3425
4278
|
</div>
|
|
3426
4279
|
</div>
|
|
3427
4280
|
|
|
4281
|
+
<div class="title-dialog-backdrop" data-ref="image-insert-dialog" hidden
|
|
4282
|
+
role="dialog" aria-modal="true" aria-labelledby="mdzip-image-insert-dialog-heading">
|
|
4283
|
+
<div class="title-dialog image-insert-dialog">
|
|
4284
|
+
<h3 id="mdzip-image-insert-dialog-heading">Insert Image</h3>
|
|
4285
|
+
<p>Choose the Markdown inserted for this image.</p>
|
|
4286
|
+
<fieldset class="image-insert-options">
|
|
4287
|
+
<legend>Output</legend>
|
|
4288
|
+
<label>
|
|
4289
|
+
<input type="radio" name="mdzip-image-insert-mode" value="markdown"
|
|
4290
|
+
data-ref="image-insert-mode-markdown" checked />
|
|
4291
|
+
Markdown image
|
|
4292
|
+
</label>
|
|
4293
|
+
<label>
|
|
4294
|
+
<input type="radio" name="mdzip-image-insert-mode" value="html"
|
|
4295
|
+
data-ref="image-insert-mode-html" />
|
|
4296
|
+
HTML <img> with sizing
|
|
4297
|
+
</label>
|
|
4298
|
+
</fieldset>
|
|
4299
|
+
<label class="image-insert-field">
|
|
4300
|
+
<span>Alt text</span>
|
|
4301
|
+
<input type="text" data-ref="image-insert-alt" />
|
|
4302
|
+
</label>
|
|
4303
|
+
<div class="image-insert-grid">
|
|
4304
|
+
<label class="image-insert-field">
|
|
4305
|
+
<span>Size by</span>
|
|
4306
|
+
<select data-ref="image-insert-size-mode">
|
|
4307
|
+
<option value="original">Original size</option>
|
|
4308
|
+
<option value="width" selected>Width</option>
|
|
4309
|
+
<option value="height">Height</option>
|
|
4310
|
+
<option value="percent">Percent</option>
|
|
4311
|
+
</select>
|
|
4312
|
+
</label>
|
|
4313
|
+
<label class="image-insert-field">
|
|
4314
|
+
<span>Value</span>
|
|
4315
|
+
<input type="number" min="1" step="1" data-ref="image-insert-size-value" />
|
|
4316
|
+
</label>
|
|
4317
|
+
</div>
|
|
4318
|
+
<label class="image-insert-field">
|
|
4319
|
+
<span>Position</span>
|
|
4320
|
+
<select data-ref="image-insert-position">
|
|
4321
|
+
<option value="inline">Inline/default</option>
|
|
4322
|
+
<option value="left">Left</option>
|
|
4323
|
+
<option value="center">Center</option>
|
|
4324
|
+
<option value="right">Right</option>
|
|
4325
|
+
<option value="wrap-left">Wrap left</option>
|
|
4326
|
+
<option value="wrap-right">Wrap right</option>
|
|
4327
|
+
</select>
|
|
4328
|
+
</label>
|
|
4329
|
+
<div class="title-dialog-actions">
|
|
4330
|
+
<button type="button" data-action="cancel-image-insert">Cancel</button>
|
|
4331
|
+
<button type="button" class="save-title" data-ref="image-insert-confirm-btn">Insert</button>
|
|
4332
|
+
</div>
|
|
4333
|
+
</div>
|
|
4334
|
+
</div>
|
|
4335
|
+
|
|
3428
4336
|
<div class="title-dialog-backdrop" data-ref="metadata-dialog" hidden
|
|
3429
4337
|
role="dialog" aria-modal="true" aria-labelledby="mdzip-metadata-dialog-heading">
|
|
3430
4338
|
<div class="title-dialog metadata-dialog">
|