Refactoring of offline handling
This commit is contained in:
parent
7b8d8af2c1
commit
4d0f36d475
@ -17,14 +17,19 @@ export class FallbackSpacePrimitives implements SpacePrimitives {
|
|||||||
fetchFileList(): Promise<FileMeta[]> {
|
fetchFileList(): Promise<FileMeta[]> {
|
||||||
return this.primary.fetchFileList();
|
return this.primary.fetchFileList();
|
||||||
}
|
}
|
||||||
|
|
||||||
async readFile(name: string): Promise<{ data: Uint8Array; meta: FileMeta }> {
|
async readFile(name: string): Promise<{ data: Uint8Array; meta: FileMeta }> {
|
||||||
try {
|
try {
|
||||||
return await this.primary.readFile(name);
|
return await this.primary.readFile(name);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.info(
|
||||||
|
`Could not read file ${name} from primary, trying fallback`,
|
||||||
|
e,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
return this.fallback.readFile(name);
|
return await this.fallback.readFile(name);
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError: any) {
|
||||||
console.error("Error during reaFile fallback", fallbackError);
|
console.error("Error during readFile fallback", fallbackError);
|
||||||
// Fallback failed, so let's throw the original error
|
// Fallback failed, so let's throw the original error
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -34,8 +39,12 @@ export class FallbackSpacePrimitives implements SpacePrimitives {
|
|||||||
try {
|
try {
|
||||||
return await this.primary.getFileMeta(name);
|
return await this.primary.getFileMeta(name);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.info(
|
||||||
|
`Could not fetch file ${name} metadata from primary, trying fallback`,
|
||||||
|
e,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
return this.fallback.getFileMeta(name);
|
return await this.fallback.getFileMeta(name);
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
console.error("Error during getFileMeta fallback", fallbackError);
|
console.error("Error during getFileMeta fallback", fallbackError);
|
||||||
// Fallback failed, so let's throw the original error
|
// Fallback failed, so let's throw the original error
|
||||||
|
@ -21,17 +21,35 @@ export class HttpSpacePrimitives implements SpacePrimitives {
|
|||||||
options.headers = { ...options.headers, ...{ "X-Sync-Mode": "true" } };
|
options.headers = { ...options.headers, ...{ "X-Sync-Mode": "true" } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await fetch(url, options);
|
try {
|
||||||
if (result.redirected) {
|
const result = await fetch(url, options);
|
||||||
// Got a redirect, we'll assume this is due to invalid credentials and redirecting to an auth page
|
if (result.status === 503) {
|
||||||
console.log(
|
throw new Error("Offline");
|
||||||
"Got a redirect via the API so will redirect to URL",
|
}
|
||||||
result.url,
|
if (result.redirected) {
|
||||||
);
|
// Got a redirect, we'll assume this is due to invalid credentials and redirecting to an auth page
|
||||||
location.href = result.url;
|
console.log(
|
||||||
throw new Error("Invalid credentials");
|
"Got a redirect via the API so will redirect to URL",
|
||||||
|
result.url,
|
||||||
|
);
|
||||||
|
location.href = result.url;
|
||||||
|
throw new Error("Invalid credentials");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (e: any) {
|
||||||
|
// Firefox: NetworkError when attempting to fetch resource (with SW and without)
|
||||||
|
// Safari: FetchEvent.respondWith received an error: TypeError: Load failed (service worker)
|
||||||
|
// Safari: Load failed (no service worker)
|
||||||
|
// Chrome: Failed to fetch (with service worker and without)
|
||||||
|
// Common substrings: "fetch" "load failed"
|
||||||
|
const errorMessage = e.message.toLowerCase();
|
||||||
|
if (
|
||||||
|
errorMessage.includes("fetch") || errorMessage.includes("load failed")
|
||||||
|
) {
|
||||||
|
throw new Error("Offline");
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchFileList(): Promise<FileMeta[]> {
|
async fetchFileList(): Promise<FileMeta[]> {
|
||||||
|
322
web/client.ts
322
web/client.ts
@ -49,29 +49,31 @@ declare global {
|
|||||||
|
|
||||||
// TODO: Oh my god, need to refactor this
|
// TODO: Oh my god, need to refactor this
|
||||||
export class Client {
|
export class Client {
|
||||||
editorView?: EditorView;
|
system: ClientSystem;
|
||||||
pageNavigator?: PathPageNavigator;
|
|
||||||
|
|
||||||
space: Space;
|
editorView: EditorView;
|
||||||
|
|
||||||
remoteSpacePrimitives: HttpSpacePrimitives;
|
private pageNavigator!: PathPageNavigator;
|
||||||
plugSpaceRemotePrimitives: PlugSpacePrimitives;
|
|
||||||
|
private dbPrefix: string;
|
||||||
|
|
||||||
|
plugSpaceRemotePrimitives!: PlugSpacePrimitives;
|
||||||
|
localSpacePrimitives!: FilteredSpacePrimitives;
|
||||||
|
remoteSpacePrimitives!: HttpSpacePrimitives;
|
||||||
|
space!: Space;
|
||||||
|
|
||||||
saveTimeout?: number;
|
saveTimeout?: number;
|
||||||
|
|
||||||
debouncedUpdateEvent = throttle(() => {
|
debouncedUpdateEvent = throttle(() => {
|
||||||
this.eventHook
|
this.eventHook
|
||||||
.dispatchEvent("editor:updated")
|
.dispatchEvent("editor:updated")
|
||||||
.catch((e) => console.error("Error dispatching editor:updated event", e));
|
.catch((e) => console.error("Error dispatching editor:updated event", e));
|
||||||
}, 1000);
|
}, 1000);
|
||||||
system: ClientSystem;
|
|
||||||
|
|
||||||
// Track if plugs have been updated since sync cycle
|
// Track if plugs have been updated since sync cycle
|
||||||
fullSyncCompleted = false;
|
fullSyncCompleted = false;
|
||||||
|
|
||||||
// Runtime state (that doesn't make sense in viewState)
|
|
||||||
syncService: SyncService;
|
syncService: SyncService;
|
||||||
settings?: BuiltinSettings;
|
settings!: BuiltinSettings;
|
||||||
kvStore: DexieKVStore;
|
kvStore: DexieKVStore;
|
||||||
|
|
||||||
// Event bus used to communicate between components
|
// Event bus used to communicate between components
|
||||||
@ -83,13 +85,11 @@ export class Client {
|
|||||||
constructor(
|
constructor(
|
||||||
parent: Element,
|
parent: Element,
|
||||||
) {
|
) {
|
||||||
const runtimeConfig = window.silverBulletConfig;
|
|
||||||
|
|
||||||
// Generate a semi-unique prefix for the database so not to reuse databases for different space paths
|
// Generate a semi-unique prefix for the database so not to reuse databases for different space paths
|
||||||
const dbPrefix = "" + simpleHash(window.silverBulletConfig.spaceFolderPath);
|
this.dbPrefix = "" + simpleHash(window.silverBulletConfig.spaceFolderPath);
|
||||||
|
|
||||||
this.kvStore = new DexieKVStore(
|
this.kvStore = new DexieKVStore(
|
||||||
`${dbPrefix}_store`,
|
`${this.dbPrefix}_store`,
|
||||||
"data",
|
"data",
|
||||||
globalThis.indexedDB,
|
globalThis.indexedDB,
|
||||||
);
|
);
|
||||||
@ -101,55 +101,14 @@ export class Client {
|
|||||||
this.system = new ClientSystem(
|
this.system = new ClientSystem(
|
||||||
this,
|
this,
|
||||||
this.kvStore,
|
this.kvStore,
|
||||||
dbPrefix,
|
this.dbPrefix,
|
||||||
this.eventHook,
|
this.eventHook,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Setup space
|
this.initSpace();
|
||||||
this.remoteSpacePrimitives = new HttpSpacePrimitives(
|
|
||||||
location.origin,
|
|
||||||
runtimeConfig.spaceFolderPath,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.plugSpaceRemotePrimitives = new PlugSpacePrimitives(
|
|
||||||
this.remoteSpacePrimitives,
|
|
||||||
this.system.namespaceHook,
|
|
||||||
);
|
|
||||||
|
|
||||||
let fileFilterFn: (s: string) => boolean = () => true;
|
|
||||||
|
|
||||||
const localSpacePrimitives = new FilteredSpacePrimitives(
|
|
||||||
new FileMetaSpacePrimitives(
|
|
||||||
new EventedSpacePrimitives(
|
|
||||||
// Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet
|
|
||||||
new FallbackSpacePrimitives(
|
|
||||||
new IndexedDBSpacePrimitives(
|
|
||||||
`${dbPrefix}_space`,
|
|
||||||
globalThis.indexedDB,
|
|
||||||
),
|
|
||||||
this.plugSpaceRemotePrimitives,
|
|
||||||
),
|
|
||||||
this.eventHook,
|
|
||||||
),
|
|
||||||
this.system.indexSyscalls,
|
|
||||||
),
|
|
||||||
(meta) => fileFilterFn(meta.name),
|
|
||||||
async () => {
|
|
||||||
await this.loadSettings();
|
|
||||||
if (typeof this.settings?.spaceIgnore === "string") {
|
|
||||||
fileFilterFn = gitIgnoreCompiler(this.settings.spaceIgnore).accepts;
|
|
||||||
} else {
|
|
||||||
fileFilterFn = () => true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
this.space = new Space(localSpacePrimitives, this.kvStore);
|
|
||||||
this.space.watch();
|
|
||||||
|
|
||||||
this.syncService = new SyncService(
|
this.syncService = new SyncService(
|
||||||
localSpacePrimitives,
|
this.localSpacePrimitives,
|
||||||
this.plugSpaceRemotePrimitives,
|
this.plugSpaceRemotePrimitives,
|
||||||
this.kvStore,
|
this.kvStore,
|
||||||
this.eventHook,
|
this.eventHook,
|
||||||
@ -172,105 +131,33 @@ export class Client {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.openPages = new OpenPages(this.editorView);
|
this.openPages = new OpenPages(this.editorView);
|
||||||
}
|
|
||||||
|
|
||||||
get currentPage(): string | undefined {
|
|
||||||
return this.ui.viewState.currentPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
this.focus();
|
this.focus();
|
||||||
|
|
||||||
this.space.on({
|
// This constructor will always be followed by an invocatition of init()
|
||||||
pageChanged: (meta) => {
|
}
|
||||||
// Only reload when watching the current page (to avoid reloading when switching pages)
|
|
||||||
if (this.space.watchInterval && this.currentPage === meta.name) {
|
|
||||||
console.log("Page changed elsewhere, reloading");
|
|
||||||
this.flashNotification("Page changed elsewhere, reloading");
|
|
||||||
this.reloadPage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pageListUpdated: (pages) => {
|
|
||||||
this.ui.viewDispatch({
|
|
||||||
type: "pages-listed",
|
|
||||||
pages: pages,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the client
|
||||||
|
* This is a separated from the constructor to allow for async initialization
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
// Load settings
|
// Load settings
|
||||||
this.settings = await this.loadSettings();
|
this.settings = await this.loadSettings();
|
||||||
|
|
||||||
this.pageNavigator = new PathPageNavigator(
|
this.initNavigator();
|
||||||
this.settings.indexPage,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page
|
// Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page
|
||||||
try {
|
try {
|
||||||
await this.remoteSpacePrimitives.getFileMeta("SETTINGS");
|
await this.remoteSpacePrimitives.getFileMeta("SETTINGS");
|
||||||
} catch {
|
} catch {
|
||||||
console.info(
|
console.warn(
|
||||||
"Could not reach remote server, either we're offline or not authenticated",
|
"Could not reach remote server, either we're offline or not authenticated",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.reloadPlugs();
|
await this.reloadPlugs();
|
||||||
|
|
||||||
this.pageNavigator.subscribe(async (pageName, pos: number | string) => {
|
|
||||||
console.log("Now navigating to", pageName);
|
|
||||||
if (!this.editorView) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateRestored = await this.loadPage(pageName);
|
|
||||||
if (pos) {
|
|
||||||
if (typeof pos === "string") {
|
|
||||||
console.log("Navigating to anchor", pos);
|
|
||||||
|
|
||||||
// We're going to look up the anchor through a direct page store query...
|
|
||||||
// TODO: This should be extracted
|
|
||||||
const posLookup = await this.system.localSyscall(
|
|
||||||
"index.get",
|
|
||||||
[
|
|
||||||
pageName,
|
|
||||||
`a:${pageName}:${pos}`,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!posLookup) {
|
|
||||||
return this.flashNotification(
|
|
||||||
`Could not find anchor @${pos}`,
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
pos = +posLookup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.editorView.dispatch({
|
|
||||||
selection: { anchor: pos },
|
|
||||||
effects: EditorView.scrollIntoView(pos, { y: "start" }),
|
|
||||||
});
|
|
||||||
} else if (!stateRestored) {
|
|
||||||
// Somewhat ad-hoc way to determine if the document contains frontmatter and if so, putting the cursor _after it_.
|
|
||||||
const pageText = this.editorView.state.sliceDoc();
|
|
||||||
|
|
||||||
// Default the cursor to be at position 0
|
|
||||||
let initialCursorPos = 0;
|
|
||||||
const match = frontMatterRegex.exec(pageText);
|
|
||||||
if (match) {
|
|
||||||
// Frontmatter found, put cursor after it
|
|
||||||
initialCursorPos = match[0].length;
|
|
||||||
}
|
|
||||||
// By default scroll to the top
|
|
||||||
this.editorView.scrollDOM.scrollTop = 0;
|
|
||||||
this.editorView.dispatch({
|
|
||||||
selection: { anchor: initialCursorPos },
|
|
||||||
// And then scroll down if required
|
|
||||||
scrollIntoView: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loadCustomStyles().catch(console.error);
|
this.loadCustomStyles().catch(console.error);
|
||||||
|
|
||||||
// Kick off background sync
|
// Kick off background sync
|
||||||
@ -319,13 +206,134 @@ export class Client {
|
|||||||
await this.dispatchAppEvent("editor:init");
|
await this.dispatchAppEvent("editor:init");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initNavigator() {
|
||||||
|
this.pageNavigator = new PathPageNavigator(
|
||||||
|
this.settings.indexPage,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.pageNavigator.subscribe(async (pageName, pos: number | string) => {
|
||||||
|
console.log("Now navigating to", pageName);
|
||||||
|
|
||||||
|
const stateRestored = await this.loadPage(pageName);
|
||||||
|
if (pos) {
|
||||||
|
if (typeof pos === "string") {
|
||||||
|
console.log("Navigating to anchor", pos);
|
||||||
|
|
||||||
|
// We're going to look up the anchor through a direct page store query...
|
||||||
|
// TODO: This should be extracted
|
||||||
|
const posLookup = await this.system.localSyscall(
|
||||||
|
"index.get",
|
||||||
|
[
|
||||||
|
pageName,
|
||||||
|
`a:${pageName}:${pos}`,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!posLookup) {
|
||||||
|
return this.flashNotification(
|
||||||
|
`Could not find anchor @${pos}`,
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
pos = +posLookup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.editorView.dispatch({
|
||||||
|
selection: { anchor: pos },
|
||||||
|
effects: EditorView.scrollIntoView(pos, { y: "start" }),
|
||||||
|
});
|
||||||
|
} else if (!stateRestored) {
|
||||||
|
// Somewhat ad-hoc way to determine if the document contains frontmatter and if so, putting the cursor _after it_.
|
||||||
|
const pageText = this.editorView.state.sliceDoc();
|
||||||
|
|
||||||
|
// Default the cursor to be at position 0
|
||||||
|
let initialCursorPos = 0;
|
||||||
|
const match = frontMatterRegex.exec(pageText);
|
||||||
|
if (match) {
|
||||||
|
// Frontmatter found, put cursor after it
|
||||||
|
initialCursorPos = match[0].length;
|
||||||
|
}
|
||||||
|
// By default scroll to the top
|
||||||
|
this.editorView.scrollDOM.scrollTop = 0;
|
||||||
|
this.editorView.dispatch({
|
||||||
|
selection: { anchor: initialCursorPos },
|
||||||
|
// And then scroll down if required
|
||||||
|
scrollIntoView: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initSpace() {
|
||||||
|
this.remoteSpacePrimitives = new HttpSpacePrimitives(
|
||||||
|
location.origin,
|
||||||
|
window.silverBulletConfig.spaceFolderPath,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.plugSpaceRemotePrimitives = new PlugSpacePrimitives(
|
||||||
|
this.remoteSpacePrimitives,
|
||||||
|
this.system.namespaceHook,
|
||||||
|
);
|
||||||
|
|
||||||
|
let fileFilterFn: (s: string) => boolean = () => true;
|
||||||
|
|
||||||
|
this.localSpacePrimitives = new FilteredSpacePrimitives(
|
||||||
|
new FileMetaSpacePrimitives(
|
||||||
|
new EventedSpacePrimitives(
|
||||||
|
// Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet
|
||||||
|
new FallbackSpacePrimitives(
|
||||||
|
new IndexedDBSpacePrimitives(
|
||||||
|
`${this.dbPrefix}_space`,
|
||||||
|
globalThis.indexedDB,
|
||||||
|
),
|
||||||
|
this.plugSpaceRemotePrimitives,
|
||||||
|
),
|
||||||
|
this.eventHook,
|
||||||
|
),
|
||||||
|
this.system.indexSyscalls,
|
||||||
|
),
|
||||||
|
(meta) => fileFilterFn(meta.name),
|
||||||
|
async () => {
|
||||||
|
// Run when a list of files has been retrieved
|
||||||
|
await this.loadSettings();
|
||||||
|
if (typeof this.settings?.spaceIgnore === "string") {
|
||||||
|
fileFilterFn = gitIgnoreCompiler(this.settings.spaceIgnore).accepts;
|
||||||
|
} else {
|
||||||
|
fileFilterFn = () => true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.space = new Space(this.localSpacePrimitives, this.kvStore);
|
||||||
|
|
||||||
|
this.space.on({
|
||||||
|
pageChanged: (meta) => {
|
||||||
|
// Only reload when watching the current page (to avoid reloading when switching pages)
|
||||||
|
if (this.space.watchInterval && this.currentPage === meta.name) {
|
||||||
|
console.log("Page changed elsewhere, reloading");
|
||||||
|
this.flashNotification("Page changed elsewhere, reloading");
|
||||||
|
this.reloadPage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pageListUpdated: (pages) => {
|
||||||
|
this.ui.viewDispatch({
|
||||||
|
type: "pages-listed",
|
||||||
|
pages: pages,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.space.watch();
|
||||||
|
}
|
||||||
|
|
||||||
async loadSettings(): Promise<BuiltinSettings> {
|
async loadSettings(): Promise<BuiltinSettings> {
|
||||||
let settingsText: string | undefined;
|
let settingsText: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
settingsText = (await this.space.readPage("SETTINGS")).text;
|
settingsText = (await this.space.readPage("SETTINGS")).text;
|
||||||
} catch {
|
} catch (e) {
|
||||||
console.log("No SETTINGS page, falling back to default");
|
console.info("No SETTINGS page, falling back to default", e);
|
||||||
settingsText = "```yaml\nindexPage: index\n```\n";
|
settingsText = "```yaml\nindexPage: index\n```\n";
|
||||||
}
|
}
|
||||||
const settings = parseYamlSettings(settingsText!) as BuiltinSettings;
|
const settings = parseYamlSettings(settingsText!) as BuiltinSettings;
|
||||||
@ -336,6 +344,10 @@ export class Client {
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get currentPage(): string | undefined {
|
||||||
|
return this.ui.viewState.currentPage;
|
||||||
|
}
|
||||||
|
|
||||||
save(immediate = false): Promise<void> {
|
save(immediate = false): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (this.saveTimeout) {
|
if (this.saveTimeout) {
|
||||||
@ -355,7 +367,7 @@ export class Client {
|
|||||||
this.space
|
this.space
|
||||||
.writePage(
|
.writePage(
|
||||||
this.currentPage,
|
this.currentPage,
|
||||||
this.editorView!.state.sliceDoc(0),
|
this.editorView.state.sliceDoc(0),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.then(async (meta) => {
|
.then(async (meta) => {
|
||||||
@ -583,7 +595,7 @@ export class Client {
|
|||||||
// Some other modal UI element is visible, don't focus editor now
|
// Some other modal UI element is visible, don't focus editor now
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.editorView!.focus();
|
this.editorView.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigate(
|
async navigate(
|
||||||
@ -616,10 +628,6 @@ export class Client {
|
|||||||
async loadPage(pageName: string): Promise<boolean> {
|
async loadPage(pageName: string): Promise<boolean> {
|
||||||
const loadingDifferentPage = pageName !== this.currentPage;
|
const loadingDifferentPage = pageName !== this.currentPage;
|
||||||
const editorView = this.editorView;
|
const editorView = this.editorView;
|
||||||
if (!editorView) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousPage = this.currentPage;
|
const previousPage = this.currentPage;
|
||||||
|
|
||||||
// Persist current page state and nicely close page
|
// Persist current page state and nicely close page
|
||||||
@ -644,12 +652,20 @@ export class Client {
|
|||||||
throw new Error("Got HTML page, not markdown");
|
throw new Error("Got HTML page, not markdown");
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// Not found, new page
|
if (e.message.includes("Not found")) {
|
||||||
console.log("Creating new page", pageName);
|
// Not found, new page
|
||||||
doc = {
|
console.log("Page doesn't exist, creating new page:", pageName);
|
||||||
text: "",
|
doc = {
|
||||||
meta: { name: pageName, lastModified: 0, perm: "rw" } as PageMeta,
|
text: "",
|
||||||
};
|
meta: { name: pageName, lastModified: 0, perm: "rw" } as PageMeta,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.error("Could not load page", pageName, e);
|
||||||
|
doc = {
|
||||||
|
text: `**ERROR**: ${e.message}`,
|
||||||
|
meta: { name: pageName, lastModified: 0, perm: "ro" } as PageMeta,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editorState = createEditorState(
|
const editorState = createEditorState(
|
||||||
@ -737,7 +753,7 @@ export class Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getContext(): string | undefined {
|
getContext(): string | undefined {
|
||||||
const state = this.editorView!.state;
|
const state = this.editorView.state;
|
||||||
const selection = state.selection.main;
|
const selection = state.selection.main;
|
||||||
if (selection.empty) {
|
if (selection.empty) {
|
||||||
return syntaxTree(state).resolveInner(selection.from).type.name;
|
return syntaxTree(state).resolveInner(selection.from).type.name;
|
||||||
|
@ -170,7 +170,7 @@ export function admonitionPlugin(editor: Client) {
|
|||||||
widget: new AdmonitionIconWidget(
|
widget: new AdmonitionIconWidget(
|
||||||
iconRange.from + 1,
|
iconRange.from + 1,
|
||||||
admonitionType,
|
admonitionType,
|
||||||
editor.editorView!,
|
editor.editorView,
|
||||||
),
|
),
|
||||||
inclusive: true,
|
inclusive: true,
|
||||||
}).range(iconRange.from, iconRange.to),
|
}).range(iconRange.from, iconRange.to),
|
||||||
|
@ -49,7 +49,7 @@ export function cleanCommandLinkPlugin(editor: Client) {
|
|||||||
(e) => {
|
(e) => {
|
||||||
if (e.altKey) {
|
if (e.altKey) {
|
||||||
// Move cursor into the link
|
// Move cursor into the link
|
||||||
return editor.editorView!.dispatch({
|
return editor.editorView.dispatch({
|
||||||
selection: { anchor: from + 2 },
|
selection: { anchor: from + 2 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -118,15 +118,15 @@ export function attachmentExtension(editor: Client) {
|
|||||||
// Only do rich text paste if shift is NOT down
|
// Only do rich text paste if shift is NOT down
|
||||||
if (richText && !shiftDown) {
|
if (richText && !shiftDown) {
|
||||||
// Are we in a fencede code block?
|
// Are we in a fencede code block?
|
||||||
const editorText = editor.editorView!.state.sliceDoc();
|
const editorText = editor.editorView.state.sliceDoc();
|
||||||
const tree = lezerToParseTree(
|
const tree = lezerToParseTree(
|
||||||
editorText,
|
editorText,
|
||||||
syntaxTree(editor.editorView!.state).topNode,
|
syntaxTree(editor.editorView.state).topNode,
|
||||||
);
|
);
|
||||||
addParentPointers(tree);
|
addParentPointers(tree);
|
||||||
const currentNode = nodeAtPos(
|
const currentNode = nodeAtPos(
|
||||||
tree,
|
tree,
|
||||||
editor.editorView!.state.selection.main.from,
|
editor.editorView.state.selection.main.from,
|
||||||
);
|
);
|
||||||
if (currentNode) {
|
if (currentNode) {
|
||||||
const fencedParentNode = findParentMatching(
|
const fencedParentNode = findParentMatching(
|
||||||
@ -141,7 +141,7 @@ export function attachmentExtension(editor: Client) {
|
|||||||
|
|
||||||
const markdown = striptHtmlComments(turndownService.turndown(richText))
|
const markdown = striptHtmlComments(turndownService.turndown(richText))
|
||||||
.trim();
|
.trim();
|
||||||
const view = editor.editorView!;
|
const view = editor.editorView;
|
||||||
const selection = view.state.selection.main;
|
const selection = view.state.selection.main;
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes: [
|
changes: [
|
||||||
@ -221,11 +221,11 @@ export function attachmentExtension(editor: Client) {
|
|||||||
if (mimeType.startsWith("image/")) {
|
if (mimeType.startsWith("image/")) {
|
||||||
attachmentMarkdown = `![](${encodeURI(finalFileName)})`;
|
attachmentMarkdown = `![](${encodeURI(finalFileName)})`;
|
||||||
}
|
}
|
||||||
editor.editorView!.dispatch({
|
editor.editorView.dispatch({
|
||||||
changes: [
|
changes: [
|
||||||
{
|
{
|
||||||
insert: attachmentMarkdown,
|
insert: attachmentMarkdown,
|
||||||
from: editor.editorView!.state.selection.main.from,
|
from: editor.editorView.state.selection.main.from,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -41,7 +41,7 @@ class IFrameWidget extends WidgetType {
|
|||||||
iframe.style.height = data.height + "px";
|
iframe.style.height = data.height + "px";
|
||||||
break;
|
break;
|
||||||
case "setBody":
|
case "setBody":
|
||||||
this.editor.editorView!.dispatch({
|
this.editor.editorView.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
from: this.from,
|
from: this.from,
|
||||||
to: this.to,
|
to: this.to,
|
||||||
@ -50,7 +50,7 @@ class IFrameWidget extends WidgetType {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "blur":
|
case "blur":
|
||||||
this.editor.editorView!.dispatch({
|
this.editor.editorView.dispatch({
|
||||||
selection: { anchor: this.from },
|
selection: { anchor: this.from },
|
||||||
});
|
});
|
||||||
this.editor.focus();
|
this.editor.focus();
|
||||||
|
@ -26,7 +26,7 @@ class TableViewWidget extends WidgetType {
|
|||||||
// Pulling data-pos to put the cursor in the right place, falling back
|
// Pulling data-pos to put the cursor in the right place, falling back
|
||||||
// to the start of the table.
|
// to the start of the table.
|
||||||
const dataAttributes = (e.target as any).dataset;
|
const dataAttributes = (e.target as any).dataset;
|
||||||
this.editor.editorView!.dispatch({
|
this.editor.editorView.dispatch({
|
||||||
selection: {
|
selection: {
|
||||||
anchor: dataAttributes.pos ? +dataAttributes.pos : this.pos,
|
anchor: dataAttributes.pos ? +dataAttributes.pos : this.pos,
|
||||||
},
|
},
|
||||||
|
@ -90,7 +90,7 @@ export function cleanWikiLinkPlugin(editor: Client) {
|
|||||||
callback: (e) => {
|
callback: (e) => {
|
||||||
if (e.altKey) {
|
if (e.altKey) {
|
||||||
// Move cursor into the link
|
// Move cursor into the link
|
||||||
return editor.editorView!.dispatch({
|
return editor.editorView.dispatch({
|
||||||
selection: { anchor: from + 2 },
|
selection: { anchor: from + 2 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -26,12 +26,12 @@ export class MainUI {
|
|||||||
constructor(private editor: Client) {
|
constructor(private editor: Client) {
|
||||||
// Make keyboard shortcuts work even when the editor is in read only mode or not focused
|
// Make keyboard shortcuts work even when the editor is in read only mode or not focused
|
||||||
globalThis.addEventListener("keydown", (ev) => {
|
globalThis.addEventListener("keydown", (ev) => {
|
||||||
if (!editor.editorView?.hasFocus) {
|
if (!editor.editorView.hasFocus) {
|
||||||
if ((ev.target as any).closest(".cm-editor")) {
|
if ((ev.target as any).closest(".cm-editor")) {
|
||||||
// In some cm element, let's back out
|
// In some cm element, let's back out
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (runScopeHandlers(editor.editorView!, ev, "editor")) {
|
if (runScopeHandlers(editor.editorView, ev, "editor")) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,11 +70,9 @@ export class MainUI {
|
|||||||
}, [viewState.currentPage]);
|
}, [viewState.currentPage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editor.editorView) {
|
editor.tweakEditorDOM(
|
||||||
editor.tweakEditorDOM(
|
editor.editorView.contentDOM,
|
||||||
editor.editorView.contentDOM,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [viewState.uiOptions.forcedROMode]);
|
}, [viewState.uiOptions.forcedROMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -190,7 +188,7 @@ export class MainUI {
|
|||||||
onRename={async (newName) => {
|
onRename={async (newName) => {
|
||||||
if (!newName) {
|
if (!newName) {
|
||||||
// Always move cursor to the start of the page
|
// Always move cursor to the start of the page
|
||||||
editor.editorView?.dispatch({
|
editor.editorView.dispatch({
|
||||||
selection: { anchor: 0 },
|
selection: { anchor: 0 },
|
||||||
});
|
});
|
||||||
editor.focus();
|
editor.focus();
|
||||||
|
@ -75,7 +75,7 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
|
|||||||
boost: def.slashCommand.boost,
|
boost: def.slashCommand.boost,
|
||||||
apply: () => {
|
apply: () => {
|
||||||
// Delete slash command part
|
// Delete slash command part
|
||||||
this.editor.editorView?.dispatch({
|
this.editor.editorView.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
from: prefix!.from + prefixText.indexOf("/"),
|
from: prefix!.from + prefixText.indexOf("/"),
|
||||||
to: ctx.pos,
|
to: ctx.pos,
|
||||||
|
@ -106,7 +106,12 @@ self.addEventListener("fetch", (event: any) => {
|
|||||||
// Must be a page URL, let's serve index.html which will handle it
|
// Must be a page URL, let's serve index.html which will handle it
|
||||||
return (await caches.match(precacheFiles["/"])) || fetch(request);
|
return (await caches.match(precacheFiles["/"])) || fetch(request);
|
||||||
}
|
}
|
||||||
})(),
|
})().catch((e) => {
|
||||||
|
console.warn("[Service worker]", "Fetch failed:", e);
|
||||||
|
return new Response("Offline", {
|
||||||
|
status: 503, // Service Unavailable
|
||||||
|
});
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -19,13 +19,13 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
|||||||
return editor.currentPage!;
|
return editor.currentPage!;
|
||||||
},
|
},
|
||||||
"editor.getText": () => {
|
"editor.getText": () => {
|
||||||
return editor.editorView?.state.sliceDoc();
|
return editor.editorView.state.sliceDoc();
|
||||||
},
|
},
|
||||||
"editor.getCursor": (): number => {
|
"editor.getCursor": (): number => {
|
||||||
return editor.editorView!.state.selection.main.from;
|
return editor.editorView.state.selection.main.from;
|
||||||
},
|
},
|
||||||
"editor.getSelection": (): { from: number; to: number } => {
|
"editor.getSelection": (): { from: number; to: number } => {
|
||||||
return editor.editorView!.state.selection.main;
|
return editor.editorView.state.selection.main;
|
||||||
},
|
},
|
||||||
"editor.save": () => {
|
"editor.save": () => {
|
||||||
return editor.save(true);
|
return editor.save(true);
|
||||||
@ -97,7 +97,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
"editor.insertAtPos": (_ctx, text: string, pos: number) => {
|
"editor.insertAtPos": (_ctx, text: string, pos: number) => {
|
||||||
editor.editorView!.dispatch({
|
editor.editorView.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
insert: text,
|
insert: text,
|
||||||
from: pos,
|
from: pos,
|
||||||
@ -105,7 +105,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
"editor.replaceRange": (_ctx, from: number, to: number, text: string) => {
|
"editor.replaceRange": (_ctx, from: number, to: number, text: string) => {
|
||||||
editor.editorView!.dispatch({
|
editor.editorView.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
insert: text,
|
insert: text,
|
||||||
from: from,
|
from: from,
|
||||||
@ -114,13 +114,13 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
"editor.moveCursor": (_ctx, pos: number, center = false) => {
|
"editor.moveCursor": (_ctx, pos: number, center = false) => {
|
||||||
editor.editorView!.dispatch({
|
editor.editorView.dispatch({
|
||||||
selection: {
|
selection: {
|
||||||
anchor: pos,
|
anchor: pos,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (center) {
|
if (center) {
|
||||||
editor.editorView!.dispatch({
|
editor.editorView.dispatch({
|
||||||
effects: [
|
effects: [
|
||||||
EditorView.scrollIntoView(
|
EditorView.scrollIntoView(
|
||||||
pos,
|
pos,
|
||||||
@ -133,8 +133,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editor.setSelection": (_ctx, from: number, to: number) => {
|
"editor.setSelection": (_ctx, from: number, to: number) => {
|
||||||
const editorView = editor.editorView!;
|
editor.editorView.dispatch({
|
||||||
editorView.dispatch({
|
|
||||||
selection: {
|
selection: {
|
||||||
anchor: from,
|
anchor: from,
|
||||||
head: to,
|
head: to,
|
||||||
@ -143,7 +142,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
|||||||
},
|
},
|
||||||
|
|
||||||
"editor.insertAtCursor": (_ctx, text: string) => {
|
"editor.insertAtCursor": (_ctx, text: string) => {
|
||||||
const editorView = editor.editorView!;
|
const editorView = editor.editorView;
|
||||||
const from = editorView.state.selection.main.from;
|
const from = editorView.state.selection.main.from;
|
||||||
editorView.dispatch({
|
editorView.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
@ -156,7 +155,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
"editor.dispatch": (_ctx, change: Transaction) => {
|
"editor.dispatch": (_ctx, change: Transaction) => {
|
||||||
editor.editorView!.dispatch(change);
|
editor.editorView.dispatch(change);
|
||||||
},
|
},
|
||||||
"editor.prompt": (
|
"editor.prompt": (
|
||||||
_ctx,
|
_ctx,
|
||||||
@ -182,7 +181,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
"editor.vimEx": (_ctx, exCommand: string) => {
|
"editor.vimEx": (_ctx, exCommand: string) => {
|
||||||
const cm = vimGetCm(editor.editorView!)!;
|
const cm = vimGetCm(editor.editorView)!;
|
||||||
return Vim.handleEx(cm, exCommand);
|
return Vim.handleEx(cm, exCommand);
|
||||||
},
|
},
|
||||||
// Sync
|
// Sync
|
||||||
@ -191,19 +190,19 @@ export function editorSyscalls(editor: Client): SysCallMapping {
|
|||||||
},
|
},
|
||||||
// Folding
|
// Folding
|
||||||
"editor.fold": () => {
|
"editor.fold": () => {
|
||||||
foldCode(editor.editorView!);
|
foldCode(editor.editorView);
|
||||||
},
|
},
|
||||||
"editor.unfold": () => {
|
"editor.unfold": () => {
|
||||||
unfoldCode(editor.editorView!);
|
unfoldCode(editor.editorView);
|
||||||
},
|
},
|
||||||
"editor.toggleFold": () => {
|
"editor.toggleFold": () => {
|
||||||
toggleFold(editor.editorView!);
|
toggleFold(editor.editorView);
|
||||||
},
|
},
|
||||||
"editor.foldAll": () => {
|
"editor.foldAll": () => {
|
||||||
foldAll(editor.editorView!);
|
foldAll(editor.editorView);
|
||||||
},
|
},
|
||||||
"editor.unfoldAll": () => {
|
"editor.unfoldAll": () => {
|
||||||
unfoldAll(editor.editorView!);
|
unfoldAll(editor.editorView);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user