-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathNav.js
261 lines (246 loc) · 12.3 KB
/
Nav.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/* global DwebArchive */
import ReactDOM from 'react-dom';
import React from 'react';
// Other IA repositories
import { homeQuery, ObjectFilter } from '@internetarchive/dweb-archivecontroller';
import { I18nSpan, currentISO, getLanguage } from './ia-components/dweb-index';
// This repository
import ArchiveBase from './ArchiveBase';
import { Page } from './components/Page';
const canonicaljson = require('@stratumn/canonicaljson');
const debug = require('debug')('dweb-archive:Nav');
function URLSearchParamsEntries(sp) {
const res = {};
// Handle parameters known to be arrays
['transport', 'paused'].forEach(k => { res[k] = []; });
sp.forEach((v, k) => {
if (Array.isArray(res[k])) {
res[k].push(v);
} else {
res[k] = v;
}
});
return res;
}
function pushHistory(...optss) {
// Note opts should NOT be urlencoded, it can be URLSearchParams in which case handled specially
// Note - searchparams is a URLSearchParams, you can't do Object.keys or Object.entries on it but can do "for x of"
// History is tricky .... take care of: SW (with Base set) \ !SW; file | http; cases
// when loaded from file, non SW window.location.origin = document.location.origin = "file://" and document.baseURI is unset
// Combine possibly multiple objects (simplifies calling)
const optsFunctional = ['wanthistory', 'noCache']; // opts used by navSearch and factory, dont save or restore
const optsCombined = Object.assign({}, ...optss.map(opts => (opts instanceof URLSearchParams ? URLSearchParamsEntries(opts) : opts)));
const opts = ObjectFilter(optsCombined, // Set of opts want in history etc
(k, v) => ((typeof v !== 'undefined') && (v !== null) && !optsFunctional.includes(k)));
// Filter opts to various kinds needed
// Known opts in url.search to pass through include: tab, query
// paused - used to be ignored here, and retrieved from transport, but we only use it on startup
// - in archive.html or bootstrap.html, so can still pass it around here, just ignoring it.
// TODO simplify const's only used once after testing
if (optsCombined.wanthistory) { // Cant use opts.wanthistory as its been filtered out)
// Pull out identifier and query
const identifier = opts.identifier || opts.item; // Currently uses item=foo in URLs, will migrate to identifier=foo
const query = opts.query;
const optsInDetailsUrl = ['item', 'identifier', 'download', 'page']; // Opts that are specially placed in Details URL
const optsDetails = ObjectFilter(opts, (k, unusedV) => !optsInDetailsUrl.includes(k));
// Setup url and title and state for pushing
const historyTitle = `Internet Archive ${query ? ('? ' + query) : identifier ? ('- ' + identifier) : ''}`;
const url = new URL(window.location);
// Ideally we'd like to be on a service that supports /arc but if it doesnt we've got an alternative.
const supportsDetails = !(url.origin === 'file://' || url.pathname.startsWith('/ipfs/') || url.pathname.startsWith('/ipns/'));
url.pathname = (!supportsDetails) ? window.location.pathname
: query ? '/details'
: `/${opts.download ? 'download' : 'details'}${identifier ? '/' + identifier : ''}${opts.page ? '/page/' + opts.page : ''}`;
const combinedparams = Object.assign({}, (!supportsDetails || query) ? opts : optsDetails); // For now, not putting persistent state in URL (was Nav.state) as first parm
const usp = new URLSearchParams();
Object.entries(combinedparams).forEach(
kv => (Array.isArray(kv[1])
? kv[1].forEach(v => usp.append(kv[0], v))
: usp.append(kv[0], kv[1]))
);
// noinspection JSValidateTypes
url.search = usp;
/* eslint-disable-next-line no-restricted-globals */
history.pushState(opts, historyTitle, url.href);
}
return opts; // Useful to caller
}
function renderPage({ item = undefined, message = undefined }) {
// Shortcut ...
// opts = { item (optional), message (optional) }
DwebArchive.page.setState({ item, message });
}
export default class Nav {
/**
* Navigate to a search
*
* @param q string to search for e.g. 'foo'
* or object e.g. {collection: foo, title: bar}
* or string representing search in form URL wants e.g. 'collection:"foo" AND title:"bar"'
* @param opts {
* sort STRING || [STRING] e.g. "-downloads"
* rows INT number of rows wanted in result
* noCache BOOL true to skip cache and reload if possible
* }
*/
static navSearch(q, opts = {}) {
debug('Navigating to Search for %s', q);
const { noCache = false } = opts;
const opts1 = { ...opts, query: q };
renderPage({ message: <I18nSpan en="Loading search" /> });
const s = new ArchiveBase(opts1); // Wants {query, sort, rows, noCache}
s.fetch_query({ noCache }, (unusedErr, unusedMembers) => {
// Ignoring error and rendering anyway, maybe want to display instead, but not sure ?
pushHistory(opts1); // Note this takes account of wantHistory //TODO-SEARCH test this works see window.onpopstate
renderPage({ item: s });
}); // Should throw error if fails to fetch //TODO-RELOAD fetch_query ignores noCache currently
}
static onclickSearch(q) {
// Build the onclick part of a search, q can be a string or an object e.g. {creator: "Foo bar", sort: "-date"}
// Its passed an object in various places
return `Nav.navSearchOnClick(${canonicaljson.stringify(q)}); return false`;
}
// noinspection JSUnusedGlobalSymbols
static navSearchOnClick(encodedQ) {
// Shortcut while onclickSearch is passing a string
const { query, sort } = canonicaljson.parse(encodedQ); // Undo encoding { query, sort }
return this.navSearch(query, { sort, wanthistory: true }); // TODO-SEARCH test on Date switcher bar
}
/**
* Fetch and render an ArchiveItem - includes Collections, but not Search (see navSearch)
*
* @param identifier
* @param opts {
* wanthistory: if set build a new entry in history
* download: Want the download directory version of the details page
* page: Relevant if its the book reader (note this might not get all the way through)
* reload: True if should use Cache-Control:no-cache to fetch (relevant in dweb-mirror when reloading)
* }
* @returns {Promise<ARCHIVEITEM>}
*/
static async factory(identifier, ...optss) {
const opts = pushHistory(...optss, { identifier });
const { download = undefined, page = undefined, noCache = undefined } = opts;
renderPage({ message: <I18nSpan en="Loading">{' ' + identifier}</I18nSpan> });
window.loopguard = identifier; // Tested in dweb-transport/httptools, will cancel any old loops - this is a kludge to get around a Chrome bug/feature
let item; // Set below, but keep it here for error handling
try {
if (!identifier || (identifier === 'home')) {
item = new ArchiveBase({ identifier: 'home', query: homeQuery, sort: '-downloads' });
await item.fetch_metadata({ noCache });
await item.fetch_query({ noCache });
renderPage({ item });
} else if (['local', 'settings'].includes(identifier)) { // SEE-OTHER-ADD-SPECIAL-PAGE in dweb-mirror dweb-archive dweb-archivecontroller
item = new ArchiveBase({ identifier: identifier });
await item.fetch_metadata(); // Intentionally not passing noCache
renderPage({ item });
} else {
item = new ArchiveBase({ identifier: identifier, page, download, noCache });
await item.fetch_metadata({ noCache }); // Note, dont do fetch_query as will expand to ArchiveMemberSearch which will confuse the export
if (!item.metadata) {
item.message = (
<>
<I18nSpan en="item" />
{' '}
{identifier}
<I18nSpan en="cannot be found or does not have metadata" />
</>
);
}
if (!item.message && item.metadata && !['texts', 'image', 'audio', 'etree', 'movies', 'collection', 'account'].includes(item.metadata.mediatype)) {
item.message = (
<I18nSpan en="Unsupported mediatype">
:
{item.metadata.mediatype}
</I18nSpan>
);
}
if (!item.message) {
await item.fetch_query({ noCache }); // Should throw error if fails to fetch //TODO-RELOAD fetch_query ignores noCache currently
}
renderPage({ item, message: item.message });
}
} catch (err) {
debug('ERROR: Nav.factory detected error %o', err);
renderPage({ item, message: err.message }); // Item may or may not be set TODO-I18n future could handle error messages here or where generated
}
}
/**
* Set global state that persists between what would normally be pages and is remembered across pages and history
* @param optss [{}]
*/
static setState(...optss) {
if (!this.state) this.state = {};
const persistentState = ['transport', 'mirror', 'paused', 'lang']; // Note that transport and paused are arrays
const combinedOpts = Object.assign({},
this.state,
...optss.map(opts => (opts instanceof URLSearchParams
? URLSearchParamsEntries(opts)
: opts)));
this.state = ObjectFilter(combinedOpts, (k, v) => (persistentState.includes(k) && (typeof v !== 'undefined') && (v !== null) && ((!Array.isArray(v)) || v.length))); // Dont keep undefined state, will end up in URLs
return ObjectFilter(combinedOpts, (k, unusedV) => !persistentState.includes(k)); // return any opts not persistent
}
/**
* Create object based on options passed in URL - this is only called from archive.html
* Gets language file if required
*
* opts {
* query: query as string "foo", object {collection:foo, title:bar} or string 'collection:"foo" AND title:"bar"'
* sort: STRING
* identifier||item: STRING (item is deprecated)
* download: True or 1 if want download directory instead
* Anything else is passed to factory
*/
static metaFactory(opts) {
getLanguage('en', (unusedErr) => { // Always get english - needed in case strings are missing from language.
getLanguage(currentISO(this.state.lang || 'en'), (err) => { // Get language used (getLanguage won't duplicate fetch if it is 'en')
// If lang set, then make sure in currentISO and fetch from server (reqd by archive.html before page loaded in metafactory)
if (err) {
debug('ERROR cannot set language to %s falling back to english: %o', this.state.lang, err);
currentISO('en');
}
this._metafactory(opts);
});
});
}
static _metafactory(opts) {
// TODO maybe dont need this metafactory, and can do at the body level and/or write Page in archive.html
const destn = document.getElementById('main'); // Blank window (except Nav) as loading
const message = <I18nSpan en="LOADING STARTING" />;
const els = <Page message={message} />;
ReactDOM.render(els, destn);
// Assumes rendering is sync
/* eslint-disable-next-line no-console */
console.assert(typeof DwebArchive.page !== 'undefined', 'Assuming ReactDOM.render is sync');
const { query, item, download } = opts;
let { identifier } = opts;
identifier = identifier || item;
const opts1 = ObjectFilter(opts, (k, unusedV) => !['query', 'item', 'identifier', 'download'].includes(k));
opts1.wanthistory = true;
if (query) {
// noinspection JSIgnoredPromiseFromCall
this.navSearch(query, opts1); // Intentionally passing transport, paused, etc that are used above
} else if (download) { // Note only works for downloading items, not files - can add later if reqd
// noinspection JSIgnoredPromiseFromCall
this.factory(identifier, opts1, { download: 1 });
} else {
// noinspection JSIgnoredPromiseFromCall
this.factory(identifier || 'home', opts1);
}
}
}
/* eslint-disable-next-line func-names */
window.onpopstate = function (event) {
debug('Going back to: %s %o', document.location, event.state);
const identifier = event.state && (event.state.identifier || event.state.item || event.state.identifier); // item in URL, identifier, identifier future
const stateOpts = Object.assign({}, event.state, { wanthistory: false });
if (event.state) {
if (event.state.query) {
// noinspection JSIgnoredPromiseFromCall
Nav.navSearch(event.state.query, stateOpts);
} else {
// noinspection JSIgnoredPromiseFromCall
Nav.factory(identifier || 'home', stateOpts);
}
}
};