diff --git a/src/pill.js b/src/pill.js index 7ebf5b1..c22c06e 100644 --- a/src/pill.js +++ b/src/pill.js @@ -1,213 +1,159 @@ -function shouldServeDefault(href) { - return href.origin === location.origin -} - -function createPage(title, content, status, timestamp) { - return { - title: title || '', - content: content || '', - status: status || 0, - timestamp: timestamp || new Date(), - } -} - -function setContent(root, page) { - document.title = page.title - root.innerHTML = page.content -} - -function fromResponse(selector, response, text) { - var fragment = document.createDocumentFragment() - var fragRoot = document.createElement('html') - fragment.appendChild(fragRoot) - fragRoot.innerHTML = text - - var title = fragRoot.querySelector('title').textContent - var root = fragRoot.querySelector(selector) - var content = root ? root.innerHTML : '' - - return {title: title, content: content} -} - -function updateState(state, url, title, push) { - if (push) { - history.pushState(state || {}, title, url) - } - else { - history.replaceState(state || {}, title, url) - } -} - -function defaultErrorHandler() { - return { - title: 'Error', - content: '

Error

Ooops. Something went wrong

', - code: 500, - timestamp: new Date(), - } -} - -function scrollToAnchor(name) { - requestAnimationFrame(function () { - var anchor - if (name in document.anchors) { - anchor = document.anchors[name] - } - else { - anchor = document.getElementById(anchor) - } - - if (anchor) { - anchor.scrollIntoView(true) - } - }) -} - -function noop() {} - -function normalizePathname(pathname) { - return '/' + pathname.replace(/\/+/g, '/').replace(/^\/|\/$/g, '') -} - -function keyFromUrlDefault(url) { - return normalizePathname(url.pathname) + url.search -} - +import { + createPage, + defaultErrorHandler, + fromResponse, + keyFromUrlDefault, + noop, + scrollToAnchor, + setContent, + shouldServeDefault, + updateState +} from "./utils"; + +/** + * + * @param {string} selector + * @param {object} options + */ export default function pill(selector, options) { - if (typeof window.history.pushState !== 'function') { - return + if (typeof window.history.pushState !== "function") { + return; } - options = options || {} - var onReady = options.onReady || noop - var onLoading = options.onLoading || noop - var onUnmounting = options.onUnmounting || noop - var onMounting = options.onMounting || noop - var onError = options.onError || console.error.bind(console) - var keyFromUrl = options.keyFromUrl || keyFromUrlDefault - var fromError = options.fromError || defaultErrorHandler - var shouldServe = options.shouldServe || shouldServeDefault - var shouldReload = options.shouldReload || noop - - var current = 0 - var isLoading = false - - var element = document.querySelector(selector) - if (! element) { - throw new Error('Element "' + selector + '" not found') + options = options || {}; + var onReady = options.onReady || noop; + var onLoading = options.onLoading || noop; + var onUnmounting = options.onUnmounting || noop; + var onMounting = options.onMounting || noop; + var onError = options.onError || console.error.bind(console); + var keyFromUrl = options.keyFromUrl || keyFromUrlDefault; + var fromError = options.fromError || defaultErrorHandler; + var shouldServe = options.shouldServe || shouldServeDefault; + var shouldReload = options.shouldReload || noop; + + var current = 0; + var isLoading = false; + + var element = document.querySelector(selector); + if (!element) { + throw new Error('Element "' + selector + '" not found'); } - var currentUrl = new URL(document.location) - var currentPage = createPage(document.title, element.innerHTML, 200) - var cache = {} - cache[keyFromUrl(currentUrl)] = currentPage - function render (url, page, push) { - onUnmounting(page, url, element) - updateState(null, url, page.title, push) - onMounting(page, url, element) - setContent(element, page) - onReady(page, element) + var currentUrl = new URL(document.location); + var currentPage = createPage(document.title, element.innerHTML, 200); + var cache = {}; + cache[keyFromUrl(currentUrl)] = currentPage; + function render(url, page, push) { + onUnmounting(page, url, element); + updateState(null, url, page.title, push); + onMounting(page, url, element); + setContent(element, page); + onReady(page, element); if (push && url.hash.length > 1) { - scrollToAnchor(url.hash.slice(1)) + scrollToAnchor(url.hash.slice(1)); } } // Initial scroll - updateState({scroll: window.scrollY}, currentUrl, currentPage.title, false) + updateState({ scroll: window.scrollY }, currentUrl, currentPage.title, false); function goto(url, push) { - var cacheKey = keyFromUrl(url) + var cacheKey = keyFromUrl(url); if (cacheKey in cache) { - var cachedPage = cache[cacheKey] + var cachedPage = cache[cacheKey]; if (shouldReload(cachedPage) !== true) { - render(url, cachedPage, push) - return + render(url, cachedPage, push); + return; } } - updateState(null, url, url, push) + updateState(null, url, url, push); - var requestId = ++current + var requestId = ++current; fetch(url) - .then(function (res) { - return res.text() - .then((function(text) { - return { - res: res, - text: text, + .then(function (res) { + return res.text().then(function (text) { + return { + res: res, + text: text, + }; + }); + }) + .finally(function () { + isLoading = false; + }) + .then(function (result) { + var res = result.res; + var text = result.text; + + var page = fromResponse(selector, res, text); + + cache[cacheKey] = page; + + page.status = res.status; + page.timestamp = new Date(); + + if (requestId !== current) { + return; + } + render(url, page, false); + }) + .catch(function (error) { + if (requestId === current) { + var page = fromError(error); + render(url, page, false); } - })) - }) - .finally(function() { - isLoading = false - }) - .then(function (result) { - var res = result.res - var text = result.text - - var page = fromResponse(selector, res, text) - - cache[cacheKey] = page - - page.status = res.status - page.timestamp = new Date() - - if (requestId !== current) { - return - } - render(url, page, false) - }) - .catch(function (error) { - if (requestId === current) { - var page = fromError(error) - render(url, page, false) - } - throw error - }) - // Handle errors, including received from previous requesterror handling - .catch(onError) + throw error; + }) + // Handle errors, including received from previous requesterror handling + .catch(onError); - isLoading = true - onLoading(url) + isLoading = true; + onLoading(url); } - function onClick (e) { - if (e.target.nodeName !== 'A') { - return + function onClick(e) { + if (e.target.nodeName !== "A") { + return; } - var url = new URL(e.target.href, document.location) + var url = new URL(e.target.href, document.location); - if (! shouldServe(url, e.target)) { - return + if (!shouldServe(url, e.target)) { + return; } - e.preventDefault() + e.preventDefault(); - window.scrollTo(0, 0) - goto(url, ! isLoading) + window.scrollTo(0, 0); + goto(url, !isLoading); } function onPopState(e) { - goto(new URL(document.location), false) - requestAnimationFrame(function() { - window.scrollTo(0, e.state.scroll || 0) - }) + goto(new URL(document.location), false); + requestAnimationFrame(function () { + window.scrollTo(0, e.state.scroll || 0); + }); } - var scrollDebounceTimeout + var scrollDebounceTimeout; function onScroll() { if (scrollDebounceTimeout) { - return + return; } scrollDebounceTimeout = setTimeout(function () { - updateState({scroll: window.scrollY}, document.location, document.title, false) - scrollDebounceTimeout = null - }, 100) + updateState( + { scroll: window.scrollY }, + document.location, + document.title, + false + ); + scrollDebounceTimeout = null; + }, 100); } - document.body.addEventListener('click', onClick) - window.addEventListener('popstate', onPopState) - window.addEventListener('scroll', onScroll) -} \ No newline at end of file + document.body.addEventListener("click", onClick); + window.addEventListener("popstate", onPopState); + window.addEventListener("scroll", onScroll); +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..e3f143f --- /dev/null +++ b/src/utils.js @@ -0,0 +1,128 @@ + +/** + * + * @param {string} title + * @param {HTMLString} content + * @param {ResponseCode} status + * @param {Date} timestamp + * @returns {object} + */ +export function createPage(title, content, status, timestamp) { + return { + title: title || "", + content: content || "", + status: status || 0, + timestamp: timestamp || new Date(), + }; +} + + +/** + * @returns {object} + */ +export function defaultErrorHandler() { + return { + title: "Error", + content: "

Error

Ooops. Something went wrong

", + code: 500, + timestamp: new Date(), + }; +} + +/** + * + * @param {string} selector + * @param {*} response + * @param {string} text + * @returns {object} + */ +export function fromResponse(selector, response, text) { + var fragment = document.createDocumentFragment(); + var fragRoot = document.createElement("html"); + fragment.appendChild(fragRoot); + fragRoot.innerHTML = text; + + var title = fragRoot.querySelector("title").textContent; + var root = fragRoot.querySelector(selector); + var content = root ? root.innerHTML : ""; + + return { title: title, content: content }; +} + +/** + * + * @param {Url} url + * @return {Url} + */ +export function keyFromUrlDefault(url) { + return normalizePathname(url.pathname) + url.search; +} + +/** + * @returns {void} + */ +export function noop() {} + +// used internally -- exported to test +/** + * + * @param {string} pathname + * @returns {string} + */ +export function normalizePathname(pathname) { + return "/" + pathname.replace(/\/+/g, "/").replace(/^\/|\/$/g, ""); +} + +/** + * + * @param {string} name + * @returns {void} + */ +export function scrollToAnchor(name) { + requestAnimationFrame(function () { + var anchor; + if (name in document.anchors) { + anchor = document.anchors[name]; + } else { + anchor = document.getElementById(anchor); + } + + if (anchor) { + anchor.scrollIntoView(true); + } + }); +} + +/** + * + * @param {HTMLElement} root + * @param {document} page + * @returns {void} + */ +export function setContent(root, page) { + document.title = page.title; + root.innerHTML = page.content; +} + +/** + * + * @param {Url} href + */ +export function shouldServeDefault(href) { + return href.origin === location.origin; +} + +/** + * + * @param {object} state + * @param {UrlString} url + * @param {string} title + * @param {boolean} push + */ +export function updateState(state, url, title, push) { + if (push) { + history.pushState(state || {}, title, url); + } else { + history.replaceState(state || {}, title, url); + } +}