159 lines
6.3 KiB
JavaScript
159 lines
6.3 KiB
JavaScript
(function (root, factory) {
|
|
const createSearchPreview = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = createSearchPreview;
|
|
if (root) {
|
|
root.createSearchPreview = createSearchPreview;
|
|
root.safeSearchUrl = value => {
|
|
try {
|
|
const url = new URL(value);
|
|
return ['http:', 'https:'].includes(url.protocol) ? url.href : '';
|
|
} catch (_) { return ''; }
|
|
};
|
|
}
|
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
return function createSearchPreview({ fetchJson, mutate, share, session, getSession, loadMore, onNavigate, navigationRoot, onState }) {
|
|
if (Array.isArray(session)) {
|
|
getSession = session[0];
|
|
loadMore = () => session[1].loadMore();
|
|
onNavigate = session[2];
|
|
navigationRoot = document;
|
|
}
|
|
let generation = 0;
|
|
let current = null;
|
|
let mutationRequest = null;
|
|
let shareRequest = null;
|
|
let moveRequest = null;
|
|
|
|
function sameItem(left, right) {
|
|
return left && right && left.kind === right.kind && left.repository === right.repository &&
|
|
Number(left.number) === Number(right.number);
|
|
}
|
|
|
|
function navigation(item) {
|
|
const session = typeof getSession === 'function' ? getSession() : null;
|
|
const items = Array.isArray(session?.items) ? session.items : [];
|
|
const index = items.findIndex(candidate => sameItem(candidate, item));
|
|
if (index < 0) return null;
|
|
return {
|
|
position:index + 1,
|
|
total:items.length,
|
|
hasPrevious:index > 0,
|
|
hasNext:index < items.length - 1 || session.more === true,
|
|
};
|
|
}
|
|
|
|
function publish(state) {
|
|
const position = navigation(state.item || current);
|
|
if (navigationRoot) {
|
|
const bar = navigationRoot.querySelector('.search-preview-navigation');
|
|
bar.hidden = !position;
|
|
if (position) {
|
|
navigationRoot.querySelector('#search-preview-position').textContent = position.position + ' of ' + position.total;
|
|
navigationRoot.querySelector('#previous-search-result').disabled = !position.hasPrevious;
|
|
navigationRoot.querySelector('#next-search-result').disabled = !position.hasNext;
|
|
}
|
|
}
|
|
onState(position ? { ...state, navigation:position } : state);
|
|
if (state.complete && navigationRoot) {
|
|
navigationRoot.querySelector('#search-preview-status').textContent = 'Search pass complete.';
|
|
}
|
|
}
|
|
|
|
function run(action, pending, success, detail) {
|
|
if (mutationRequest) return mutationRequest;
|
|
publish({ status: pending, item: current, detail });
|
|
mutationRequest = mutate(detail, action).then(result => {
|
|
publish({ status: success, item: current, detail, result });
|
|
return result;
|
|
}).catch(error => {
|
|
publish({ status: 'ready', item: current, detail, error });
|
|
throw error;
|
|
}).finally(() => { mutationRequest = null; });
|
|
return mutationRequest;
|
|
}
|
|
|
|
const api = {
|
|
open(item) {
|
|
generation += 1;
|
|
const requestGeneration = generation;
|
|
current = { ...item };
|
|
publish({ status: 'loading', item: current });
|
|
return fetchJson(current).then(detail => {
|
|
if (requestGeneration === generation) {
|
|
current = { ...current, ...detail };
|
|
publish({ status: 'ready', item: current, detail });
|
|
}
|
|
return detail;
|
|
}).catch(error => {
|
|
if (requestGeneration === generation) {
|
|
publish({ status: 'error', item: current, error });
|
|
}
|
|
throw error;
|
|
});
|
|
},
|
|
close() {
|
|
generation += 1;
|
|
current = null;
|
|
onState({ status: 'closed' });
|
|
},
|
|
previous() {
|
|
const session = typeof getSession === 'function' ? getSession() : null;
|
|
const items = Array.isArray(session?.items) ? session.items : [];
|
|
const index = items.findIndex(item => sameItem(item, current));
|
|
if (index <= 0) return Promise.resolve(current);
|
|
const item = items[index - 1];
|
|
if (typeof onNavigate === 'function') onNavigate(item);
|
|
return api.open(item);
|
|
},
|
|
next() {
|
|
if (moveRequest) return moveRequest;
|
|
moveRequest = (async () => {
|
|
let session = typeof getSession === 'function' ? getSession() : null;
|
|
let items = Array.isArray(session?.items) ? session.items : [];
|
|
let index = items.findIndex(item => sameItem(item, current));
|
|
if (index < 0) return current;
|
|
if (index === items.length - 1 && session?.more === true && typeof loadMore === 'function') {
|
|
await loadMore();
|
|
session = getSession();
|
|
items = Array.isArray(session?.items) ? session.items : [];
|
|
index = items.findIndex(item => sameItem(item, current));
|
|
}
|
|
if (index >= 0 && index < items.length - 1) {
|
|
const item = items[index + 1];
|
|
if (typeof onNavigate === 'function') onNavigate(item);
|
|
return api.open(item);
|
|
}
|
|
publish({ status:'ready', item:current, detail:current, complete:true });
|
|
return current;
|
|
})().finally(() => { moveRequest = null; });
|
|
return moveRequest;
|
|
},
|
|
share(url) {
|
|
if (shareRequest) return shareRequest;
|
|
if (!current || typeof share !== 'function') return Promise.reject(new Error('Sharing is unavailable.'));
|
|
const detail = current;
|
|
publish({ status:'sharing', item:current, detail });
|
|
shareRequest = share(url).then(result => {
|
|
publish({ status:result, item:current, detail });
|
|
return result;
|
|
}).catch(error => {
|
|
publish({ status:error?.name === 'AbortError' ? 'share-canceled' : 'share-error', item:current, detail, error });
|
|
throw error;
|
|
}).finally(() => { shareRequest = null; });
|
|
return shareRequest;
|
|
},
|
|
claim(detail) {
|
|
return run('claim', 'claiming', 'claimed', detail);
|
|
},
|
|
reopen(detail) {
|
|
return run('reopen', 'reopening', 'reopened', detail);
|
|
},
|
|
};
|
|
if (navigationRoot) {
|
|
navigationRoot.querySelector('#previous-search-result').addEventListener('click', () => api.previous().catch(() => {}));
|
|
navigationRoot.querySelector('#next-search-result').addEventListener('click', () => api.next().catch(() => {}));
|
|
}
|
|
return api;
|
|
};
|
|
});
|