feat: consolidate local-first mnote web runtime

This commit is contained in:
lix-2026
2026-05-28 22:01:44 +08:00
parent 7354807ee9
commit 39b9a0183a
154 changed files with 13591 additions and 12728 deletions
@@ -1,12 +1,45 @@
import {
localMarkdownDocumentIdFromRelativePath,
localMarkdownRelativePathFromDocumentId,
localizeTiptapAssetUrls,
} from './document-tiptap-conversion-runtime.js';
/**
* @typedef {Object} MNoteWorkspacePath
* @property {'mnote.workspace_path.v1'} schema
* @property {string} workspaceId
* @property {string} sourceKind
* @property {string} rootUri
* @property {string} relativePath
* @property {string} documentId
* @property {string} objectIdentity
* @property {string} assetId
* @property {string} resourceKind
*
* @typedef {Object} MNoteOpenEditorEntry
* @property {string} objectIdentity
* @property {MNoteWorkspacePath|null} workspacePath
* @property {'primary'|'secondary'} paneRole
* @property {string} editorKind
* @property {boolean} active
* @property {string} dirtyState
* @property {boolean} preview
* @property {boolean} pinned
* @property {number} lastActiveAt
*
* @typedef {Object} MNoteOpenEditorsSnapshot
* @property {'mnote.open_editors_snapshot.v1'} schema
* @property {number} generatedAt
* @property {string} activeObjectIdentity
* @property {MNoteOpenEditorEntry[]} editors
* @property {{primary: Object, secondary: Object}} groups
*/
export const createResourceTabRuntime = (dependencies = {}) => {
const {
loadRuntime,
createEditorViewBinding,
ensureLocalFolderEventChannel,
unmountEditorViewBinding,
setStatus,
documentSessionRegistry,
@@ -62,9 +95,31 @@ export const createResourceTabRuntime = (dependencies = {}) => {
return 'file';
};
const resourceTabWidthType = (entry) => {
if (!entry) return '';
const kind = String(entry.kind || '').trim();
if (kind === 'office') {
const badgeKind = String(entry.badgeKind || resourceTabBadgeKind(entry, kind) || '').trim();
if (badgeKind === 'ppt') return 'ppt';
if (badgeKind === 'sheet') return 'excel';
return 'word';
}
if (kind === 'pdf') return 'pdf';
if (kind === 'mindmap') return 'mindmap';
if (kind === 'markdown' || kind === 'text' || kind === 'code') return 'markdown';
return '';
};
const currentWebShellWorkspaceId = () => {
const explicit = typeof currentWorkspaceId === 'function' ? String(currentWorkspaceId() || '').trim() : '';
if (explicit) return explicit;
const fromBody = document.body?.dataset?.workspaceId || '';
if (fromBody) return String(fromBody).trim();
const shell = document.querySelector('.document-shell[data-workspace-id], [data-document-pane="true"][data-pane-role="primary"][data-pane-workspace-id]');
if (shell instanceof HTMLElement) {
const value = shell.getAttribute('data-workspace-id') || shell.getAttribute('data-pane-workspace-id') || '';
if (value) return value.trim();
}
try {
return currentUrl().searchParams.get('workspaceId') || '';
} catch (_) {
@@ -82,6 +137,80 @@ export const createResourceTabRuntime = (dependencies = {}) => {
return '';
};
const documentIdForPane = (paneRole) => {
const role = normalizePaneRole(paneRole);
const nodes = resourceTabHostNodes(role);
const fromTab = String(nodes.pageTab?.getAttribute?.('data-document-id') || '').trim();
if (fromTab) return fromTab;
const pane = document.querySelector(`[data-document-pane="true"][data-pane-role="${role}"]`);
if (pane instanceof HTMLElement) {
const fromPane = String(pane.getAttribute('data-pane-document-id') || '').trim();
if (fromPane) return fromPane;
const root = pane.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"][data-document-id], .document-shell[data-document-id]');
if (root instanceof HTMLElement) {
const fromRoot = String(root.getAttribute('data-document-id') || '').trim();
if (fromRoot) return fromRoot;
}
}
return role === 'primary' ? currentWebShellDocumentId() : '';
};
const currentWebShellSourceKind = () => {
try {
return currentUrl().searchParams.get('sourceKind') || '';
} catch (_) {
return '';
}
};
const currentWebShellRootUri = () => {
try {
return currentUrl().searchParams.get('rootUri') || '';
} catch (_) {
return '';
}
};
const buildWorkspacePath = ({
workspacePath,
workspaceId,
sourceKind,
rootUri,
relativePath,
documentId,
objectIdentity,
assetId,
resourceKind,
} = {}) => {
const fromInput = workspacePath && typeof workspacePath === 'object' ? workspacePath : null;
if (fromInput && String(fromInput.schema || '') === 'mnote.workspace_path.v1') {
return {
schema: 'mnote.workspace_path.v1',
workspaceId: String(fromInput.workspaceId || workspaceId || '').trim(),
sourceKind: String(fromInput.sourceKind || sourceKind || '').trim(),
rootUri: String(fromInput.rootUri || rootUri || '').trim(),
relativePath: String(fromInput.relativePath || fromInput.localRelativePath || relativePath || '').trim(),
documentId: String(fromInput.documentId || documentId || '').trim(),
objectIdentity: fromInput.objectIdentity ?? String(objectIdentity || '').trim(),
assetId: String(fromInput.assetId || assetId || '').trim(),
resourceKind: String(fromInput.resourceKind || fromInput.objectKind || resourceKind || '').trim(),
};
}
const id = String(objectIdentity || documentId || assetId || relativePath || '').trim();
if (!id) return null;
return {
schema: 'mnote.workspace_path.v1',
workspaceId: String(workspaceId || '').trim(),
sourceKind: String(sourceKind || '').trim(),
rootUri: String(rootUri || '').trim(),
relativePath: String(relativePath || '').trim(),
documentId: String(documentId || '').trim(),
objectIdentity: String(objectIdentity || '').trim(),
assetId: String(assetId || '').trim(),
resourceKind: String(resourceKind || '').trim(),
};
};
const normalizeResourceTabKind = (input) => {
const kind = String(input?.kind || '').trim().toLowerCase();
const title = String(input?.title || input?.fileName || input?.path || '').trim().toLowerCase();
@@ -121,7 +250,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
const setResourceTabLastActive = (key) => {
const entry = resourceTabRegistry.get(String(key || '').trim());
if (entry?.session) entry.session.lastActiveAt = Date.now();
if (!entry) return;
const timestamp = Date.now();
entry.lastActiveAt = timestamp;
if (entry.session) entry.session.lastActiveAt = timestamp;
};
const removeFromResourceTabMru = (paneRole, key) => {
@@ -140,6 +272,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
const resourceTabCloseGuardReason = (session) => {
if (!session) return '';
const bufferDirtyState = String(session.bufferDirtyState || '').trim();
if (bufferDirtyState === 'Dirty' || bufferDirtyState === 'Stale' || bufferDirtyState === 'Deleted' || bufferDirtyState === 'ExternalModified') {
return bufferDirtyState;
}
const hasUnsavedChanges = session.dirty
|| Boolean(session.saveTimer)
|| sessionHasRecentLocalInput(session)
@@ -150,7 +286,49 @@ export const createResourceTabRuntime = (dependencies = {}) => {
return '';
};
const syncResourceTabCloseGuard = (entry) => {
const documentSessionForPane = (paneRole, documentId) => {
const id = String(documentId || '').trim();
if (!id || !documentSessionRegistry || typeof documentSessionRegistry.values !== 'function') return null;
const role = normalizePaneRole(paneRole);
return Array.from(documentSessionRegistry.values()).find((session) => (
session
&& session.sessionKind !== 'resource'
&& String(session.documentId || '').trim() === id
&& Array.from(session.views?.values?.() || []).some((view) => normalizePaneRole(view?.runtimeDescriptor?.paneRole) === role)
)) || Array.from(documentSessionRegistry.values()).find((session) => (
session
&& session.sessionKind !== 'resource'
&& String(session.documentId || '').trim() === id
)) || null;
};
const documentSessionDirtyState = (session) => {
if (!session) return '';
const bufferDirtyState = String(session.bufferDirtyState || '').trim();
if (bufferDirtyState === 'Dirty' || bufferDirtyState === 'Stale' || bufferDirtyState === 'Deleted' || bufferDirtyState === 'ExternalModified') return bufferDirtyState;
if (session.hasExternalConflict) return 'ExternalModified';
if (session.dirty || Boolean(session.saveTimer) || sessionHasRecentLocalInput(session)) return 'Dirty';
if (session.saving) return 'Saving';
return '';
};
const resourceTabBufferStateUrl = (entry) => {
const session = entry?.session;
if (!session || session.sourceKind !== 'local_folder') return null;
const relativePath = String(session.resourcePath || entry?.workspacePath?.relativePath || '').trim();
const rootUri = String(session.rootUri || entry?.workspacePath?.rootUri || '').trim();
if (!relativePath || !rootUri) return null;
const url = new URL('/api/documents/buffer-state', window.location.origin);
url.searchParams.set('documentId', String(session.documentId || entry.objectIdentity || ''));
url.searchParams.set('sourceKind', 'local_folder');
url.searchParams.set('rootUri', rootUri);
url.searchParams.set('relativePath', relativePath);
const workspaceId = String(session.workspaceId || entry?.workspacePath?.workspaceId || '').trim();
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
return url;
};
const applyResourceTabCloseGuard = (entry) => {
if (!entry?.tab || !(entry.tab instanceof HTMLElement)) return;
const reason = resourceTabCloseGuardReason(entry.session);
if (reason) {
@@ -162,6 +340,30 @@ export const createResourceTabRuntime = (dependencies = {}) => {
}
};
const refreshResourceTabBufferState = async (entry) => {
const url = resourceTabBufferStateUrl(entry);
if (!url || !entry?.session) return null;
try {
const response = await fetch(url.toString(), { cache: 'no-store', headers: { accept: 'application/json' } });
const payload = await response.json().catch(() => null);
if (!response.ok || !payload || payload.ok !== true || !payload.result) return null;
const dirtyState = String(payload.result.dirtyState || '').trim();
if (dirtyState) entry.session.bufferDirtyState = dirtyState;
if (typeof payload.result.fileVersion === 'string' && payload.result.fileVersion.trim()) {
entry.session.fileVersion = payload.result.fileVersion.trim();
}
applyResourceTabCloseGuard(entry);
return payload.result;
} catch (_) {
return null;
}
};
const syncResourceTabCloseGuard = (entry) => {
applyResourceTabCloseGuard(entry);
void refreshResourceTabBufferState(entry);
};
const syncResourceSessionTabGuards = (session) => {
if (!session || session.sessionKind !== 'resource') return;
resourceTabRegistry.forEach((entry) => {
@@ -169,52 +371,126 @@ export const createResourceTabRuntime = (dependencies = {}) => {
});
};
const openEditorsSnapshotEntry = (entry, key) => ({
objectIdentity: String(entry?.objectIdentity || key || '').trim(),
title: String(entry?.title || '资源').trim() || '资源',
kind: normalizeResourceTabKind(entry),
badgeKind: entry?.tab instanceof HTMLElement
? String(entry.tab.getAttribute('data-mnote-tab-badge-kind') || resourceTabBadgeKind(entry, entry.kind)).trim()
: resourceTabBadgeKind(entry, entry?.kind),
active: entry?.tab instanceof HTMLElement
const openEditorsSnapshotEntry = (entry, key, generatedAt = Date.now()) => {
const active = entry?.tab instanceof HTMLElement
? entry.tab.getAttribute('aria-selected') === 'true'
: false,
dirtyGuard: resourceTabCloseGuardReason(entry?.session),
assetId: String(entry?.assetId || entry?.session?.assetId || '').trim(),
path: String(entry?.path || entry?.session?.resourcePath || '').trim(),
});
: false;
const documentId = String(entry?.documentId || entry?.ownerDocumentId || entry?.session?.ownerDocumentId || '').trim();
const workspaceId = String(entry?.workspaceId || entry?.session?.workspaceId || currentWebShellWorkspaceId() || '').trim();
const sourceKind = String(entry?.sourceKind || entry?.session?.sourceKind || currentWebShellSourceKind() || '').trim();
const rootUri = String(entry?.rootUri || entry?.session?.rootUri || currentWebShellRootUri() || '').trim();
const relativePath = String(entry?.path || entry?.session?.resourcePath || '').trim();
const objectIdentity = String(entry?.objectIdentity || key || '').trim();
const assetId = String(entry?.assetId || entry?.session?.assetId || '').trim();
const kind = normalizeResourceTabKind(entry);
const dirtyState = resourceTabCloseGuardReason(entry?.session);
return {
objectIdentity,
workspacePath: buildWorkspacePath({
workspaceId,
sourceKind,
rootUri,
relativePath,
documentId,
objectIdentity,
assetId,
resourceKind: kind,
workspacePath: entry?.workspacePath,
}),
paneRole: normalizePaneRole(entry?.paneRole),
documentId,
workspaceId,
title: String(entry?.title || '资源').trim() || '资源',
kind,
editorKind: kind,
badgeKind: entry?.tab instanceof HTMLElement
? String(entry.tab.getAttribute('data-mnote-tab-badge-kind') || resourceTabBadgeKind(entry, entry.kind)).trim()
: resourceTabBadgeKind(entry, entry?.kind),
active,
dirtyState,
dirtyGuard: dirtyState,
assetId,
path: relativePath,
lastActiveAt: Number(entry?.session?.lastActiveAt || entry?.lastActiveAt || 0) || (active ? generatedAt : 0),
preview: false,
pinned: false,
};
};
const buildOpenEditorsSnapshot = () => {
const generatedAt = Date.now();
const pageEntries = ['primary', 'secondary'].map((paneRole) => {
const nodes = resourceTabHostNodes(paneRole);
const documentId = documentIdForPane(paneRole);
const workspaceId = String(nodes.pageTab?.getAttribute?.('data-workspace-id') || currentWebShellWorkspaceId() || '').trim();
const sourceKind = currentWebShellSourceKind();
const rootUri = currentWebShellRootUri();
const relativePath = sourceKind === 'local_folder' ? localMarkdownRelativePathFromDocumentId(documentId) : '';
const objectIdentity = `page:${paneRole}`;
const active = nodes.pageTab instanceof HTMLElement
? nodes.pageTab.getAttribute('aria-selected') === 'true'
: false;
const pageSession = documentSessionForPane(paneRole, documentId);
const dirtyState = documentSessionDirtyState(pageSession);
const pageTitle = nodes.pageTab instanceof HTMLElement
? String(nodes.pageTab.querySelector('.mnote-main-tab-title')?.textContent || '页面').trim()
: '页面';
return {
objectIdentity: `page:${paneRole}`,
objectIdentity,
workspacePath: buildWorkspacePath({
workspaceId,
sourceKind,
rootUri,
relativePath,
documentId,
objectIdentity,
assetId: '',
resourceKind: 'page',
}),
paneRole,
documentId: String(nodes.pageTab?.getAttribute?.('data-document-id') || '').trim(),
workspaceId: String(nodes.pageTab?.getAttribute?.('data-workspace-id') || currentWebShellWorkspaceId() || '').trim(),
documentId,
workspaceId,
title: pageTitle || '页面',
kind: 'page',
editorKind: 'page',
badgeKind: 'code',
active: nodes.pageTab instanceof HTMLElement
? nodes.pageTab.getAttribute('aria-selected') === 'true'
: false,
dirtyGuard: '',
active,
dirtyState,
dirtyGuard: dirtyState,
lastActiveAt: active ? generatedAt : 0,
preview: false,
pinned: true,
};
}).filter((entry) => entry.paneRole === 'primary' || entry.documentId || document.querySelector('[data-document-pane="true"][data-pane-role="secondary"][data-pane-visible="true"]'));
const resources = [];
resourceTabRegistry.forEach((entry, key) => {
resources.push(openEditorsSnapshotEntry(entry, key));
resources.push(openEditorsSnapshotEntry(entry, key, generatedAt));
});
const active = [...pageEntries, ...resources].find((entry) => entry.active);
const editors = [...pageEntries, ...resources];
const groupForPane = (paneRole) => {
const role = normalizePaneRole(paneRole);
const groupEditors = editors.filter((entry) => normalizePaneRole(entry.paneRole) === role);
const groupResources = resources.filter((entry) => normalizePaneRole(entry.paneRole) === role);
const groupActive = groupEditors.find((entry) => entry.active);
return {
paneRole: role,
activeObjectIdentity: groupActive?.objectIdentity || '',
editors: groupEditors,
resourceEditors: groupResources,
};
};
const groups = {
primary: groupForPane('primary'),
secondary: groupForPane('secondary'),
};
const activeObjectIdentity = groups.primary.activeObjectIdentity || groups.secondary.activeObjectIdentity || '';
return {
schema: 'mnote.open_editors_snapshot.v1',
generatedAt: Date.now(),
activeObjectIdentity: active?.objectIdentity || '',
editors: [...pageEntries, ...resources],
generatedAt,
activeObjectIdentity,
editors,
resourceEditors: resources,
groups,
};
};
@@ -295,6 +571,29 @@ export const createResourceTabRuntime = (dependencies = {}) => {
}
};
const syncActiveResourceWidthType = (activeEntry, paneRole = 'primary') => {
const role = normalizePaneRole(paneRole);
const widthType = role === 'primary' ? resourceTabWidthType(activeEntry) : '';
const targetNodes = [document.documentElement, document.body].filter((node) => node instanceof HTMLElement);
targetNodes.forEach((node) => {
if (widthType) node.setAttribute('data-mnote-active-resource-width-type', widthType);
else node.removeAttribute('data-mnote-active-resource-width-type');
});
const shell = document.querySelector(`.document-pane[data-pane-role="${role}"] .document-shell`);
if (shell instanceof HTMLElement) {
if (widthType) shell.setAttribute('data-mnote-active-resource-width-type', widthType);
else shell.removeAttribute('data-mnote-active-resource-width-type');
}
window.dispatchEvent(new CustomEvent('mnote:active-resource-tab-changed', {
detail: {
paneRole: role,
active: Boolean(activeEntry),
widthType,
objectIdentity: String(activeEntry?.objectIdentity || '')
}
}));
};
const syncActiveResourceUrlState = (activeResource, paneRole = 'primary') => {
const role = normalizePaneRole(paneRole);
if (role !== 'primary') return;
@@ -340,6 +639,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
if (entry.panel instanceof HTMLElement) entry.panel.hidden = !active;
syncResourceTabCloseGuard(entry);
});
syncActiveResourceWidthType(activeEntry, role);
if (activeEntry) markIntendedSlashRoot(activeEntry);
if (!activeEntry && window.__mnoteIntendedSlashRoot instanceof HTMLElement) window.__mnoteIntendedSlashRoot = null;
if (role === 'primary') syncActiveResourceFileTreeRow(activeResource);
@@ -399,10 +699,11 @@ export const createResourceTabRuntime = (dependencies = {}) => {
syncOpenEditorsSnapshot();
};
const closeResourceTab = (objectIdentity) => {
const closeResourceTab = async (objectIdentity) => {
const key = String(objectIdentity || '').trim();
const entry = resourceTabRegistry.get(key);
if (!entry) return;
await refreshResourceTabBufferState(entry);
const guardReason = resourceTabCloseGuardReason(entry.session);
if (guardReason) {
console.warn(`mnote resource tab 关闭阻止: ${entry.title} (${guardReason})`);
@@ -489,6 +790,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
paneRole,
title,
kind,
badgeKind: resourceTabBadgeKind(input, kind),
tab,
panel,
view: null,
@@ -497,6 +799,21 @@ export const createResourceTabRuntime = (dependencies = {}) => {
path: String(input.path || '').trim(),
documentId: String(input.documentId || '').trim(),
ownerDocumentId: String(input.ownerDocumentId || input.documentId || '').trim(),
workspaceId: String(input.workspaceId || currentWebShellWorkspaceId() || '').trim(),
sourceKind: String(input.sourceKind || currentWebShellSourceKind() || '').trim(),
rootUri: String(input.rootUri || currentWebShellRootUri() || '').trim(),
workspacePath: buildWorkspacePath({
workspacePath: input.workspacePath,
workspaceId: input.workspaceId || currentWebShellWorkspaceId(),
sourceKind: input.sourceKind || currentWebShellSourceKind(),
rootUri: input.rootUri || currentWebShellRootUri(),
relativePath: input.path,
documentId: input.documentId,
objectIdentity,
assetId: input.assetId,
resourceKind: kind,
}),
lastActiveAt: 0,
};
};
@@ -515,6 +832,12 @@ export const createResourceTabRuntime = (dependencies = {}) => {
}
entry.mindmapRuntime = null;
}
if (entry.resourceWatchEventSource) {
try {
entry.resourceWatchEventSource.close();
} catch (_) {}
entry.resourceWatchEventSource = null;
}
if (entry.panel instanceof HTMLElement) entry.panel.replaceChildren();
};
@@ -525,6 +848,80 @@ export const createResourceTabRuntime = (dependencies = {}) => {
return url.toString();
};
const localResourceStatUrl = (rootUri, path) => {
const url = new URL('/api/local-folder/files/stat', window.location.origin);
url.searchParams.set('rootUri', rootUri || '');
url.searchParams.set('path', path || '');
return url.toString();
};
const withResourceReloadToken = (href) => {
if (!href) return href;
const url = new URL(href, window.location.origin);
url.searchParams.set('mnoteResourceReload', String(Date.now()));
return url.toString();
};
const renderPassiveResourceMissing = (entry, message) => {
if (!(entry?.panel instanceof HTMLElement)) return;
entry.panel.setAttribute('data-mnote-resource-missing', 'true');
entry.panel.innerHTML = '<div class="mnote-resource-tab-error" data-resource-tab-error="true" data-mnote-resource-missing="true"><div class="mnote-resource-tab-error-inner"><h1>资源已不可用</h1><p></p></div></div>';
const text = entry.panel.querySelector('p');
if (text instanceof HTMLElement) text.textContent = message || '本地资源文件已被删除或移动。';
};
const reloadPassiveResourceTab = (entry) => {
if (!(entry?.panel instanceof HTMLElement)) return;
entry.panel.removeAttribute('data-mnote-resource-missing');
const img = entry.panel.querySelector('img.mnote-resource-tab-image');
if (img instanceof HTMLImageElement) {
img.src = withResourceReloadToken(img.getAttribute('src') || img.src || '');
return;
}
const frame = entry.panel.querySelector('iframe.mnote-resource-tab-frame');
if (frame instanceof HTMLIFrameElement) {
frame.src = withResourceReloadToken(frame.getAttribute('src') || frame.src || '');
}
};
const installPassiveResourceWatch = (entry) => {
if (!entry || entry.session || typeof window.EventSource !== 'function') return;
const rootUri = String(entry.rootUri || '').trim();
const path = String(entry.path || '').trim();
if (!rootUri || !path) return;
if (entry.resourceWatchEventSource) {
try {
entry.resourceWatchEventSource.close();
} catch (_) {}
entry.resourceWatchEventSource = null;
}
const url = new URL('/api/local-folder/events', window.location.origin);
url.searchParams.set('rootUri', rootUri);
url.searchParams.set('resourcePath', path);
const eventSource = new EventSource(url.toString());
entry.resourceWatchEventSource = eventSource;
eventSource.addEventListener('ready', () => {
if (entry.panel instanceof HTMLElement) entry.panel.setAttribute('data-mnote-resource-watch-ready', 'true');
});
eventSource.addEventListener('change', async () => {
try {
const response = await fetch(localResourceStatUrl(rootUri, path), {
cache: 'no-store',
headers: { accept: 'application/json' },
});
const payload = await response.json().catch(() => null);
const exists = Boolean(response.ok && payload?.ok === true && payload?.result?.exists);
if (!exists) {
renderPassiveResourceMissing(entry, '本地资源文件已被删除或移动。');
return;
}
reloadPassiveResourceTab(entry);
} catch (error) {
renderPassiveResourceMissing(entry, error instanceof Error ? error.message : String(error));
}
});
};
const createResourceSession = (entry, input, readResult) => {
const resourcePath = String(input.path || '');
const tiptapDocument = localizeTiptapAssetUrls(
@@ -577,6 +974,9 @@ export const createResourceTabRuntime = (dependencies = {}) => {
error: null,
};
documentSessionRegistry.set(session.key, session);
if (typeof ensureLocalFolderEventChannel === 'function') {
ensureLocalFolderEventChannel(session);
}
return session;
};
@@ -644,6 +1044,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
img.src = href;
img.alt = entry.title;
}
installPassiveResourceWatch(entry);
return;
}
entry.panel.innerHTML = '<iframe class="mnote-resource-tab-frame" title=""></iframe>';
@@ -652,6 +1053,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
frame.title = entry.title;
frame.src = href;
}
installPassiveResourceWatch(entry);
};
const refreshExistingOfficeResourceTab = (entry, input) => {