From c6106ee33099aa265e90252c855e6583fd97fe1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Wed, 30 Mar 2022 14:59:57 +0100 Subject: [PATCH 1/3] Move `isWildcard` to top of file --- src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index ff92739..979abe9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,10 @@ function task(): Promise { return new Promise(resolve => setTimeout(resolve, 0)) } +function isWildcard(accept: string | null) { + return accept && !!accept.split(',').find(x => x.match(/^\s*\*\/\*/)) +} + async function handleData(el: IncludeFragmentElement) { observer.unobserve(el) return getData(el).then( @@ -108,10 +112,6 @@ function fetchDataWithEvents(el: IncludeFragmentElement) { ) } -function isWildcard(accept: string | null) { - return accept && !!accept.split(',').find(x => x.match(/^\s*\*\/\*/)) -} - export default class IncludeFragmentElement extends HTMLElement { static get observedAttributes(): string[] { return ['src', 'loading'] From 61087239c7f3587f7f53c0cffb2784a05fe15dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Wed, 30 Mar 2022 15:01:07 +0100 Subject: [PATCH 2/3] Move `observer` and `handleData` into the component --- src/index.ts | 97 ++++++++++++++++++++++++++-------------------------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/src/index.ts b/src/index.ts index 979abe9..552e5bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,28 +1,5 @@ const privateData = new WeakMap() -const observer = new IntersectionObserver( - entries => { - for (const entry of entries) { - if (entry.isIntersecting) { - const {target} = entry - observer.unobserve(target) - if (!(target instanceof IncludeFragmentElement)) return - if (target.loading === 'lazy') { - handleData(target) - } - } - } - }, - { - // Currently the threshold is set to 256px from the bottom of the viewport - // with a threshold of 0.1. This means the element will not load until about - // 2 keyboard-down-arrow presses away from being visible in the viewport, - // giving us some time to fetch it before the contents are made visible - rootMargin: '0px 0px 256px 0px', - threshold: 0.01 - } -) - // Functional stand in for the W3 spec "queue a task" paradigm function task(): Promise { return new Promise(resolve => setTimeout(resolve, 0)) @@ -32,27 +9,6 @@ function isWildcard(accept: string | null) { return accept && !!accept.split(',').find(x => x.match(/^\s*\*\/\*/)) } -async function handleData(el: IncludeFragmentElement) { - observer.unobserve(el) - return getData(el).then( - function (html: string) { - const template = document.createElement('template') - // eslint-disable-next-line github/no-inner-html - template.innerHTML = html - const fragment = document.importNode(template.content, true) - const canceled = !el.dispatchEvent( - new CustomEvent('include-fragment-replace', {cancelable: true, detail: {fragment}}) - ) - if (canceled) return - el.replaceWith(fragment) - el.dispatchEvent(new CustomEvent('include-fragment-replaced')) - }, - function () { - el.classList.add('is-error') - } - ) -} - function getData(el: IncludeFragmentElement) { const src = el.src let data = privateData.get(el) @@ -157,12 +113,12 @@ export default class IncludeFragmentElement extends HTMLElement { if (attribute === 'src') { // Source changed after attached so replace element. if (this.isConnected && this.loading === 'eager') { - handleData(this) + this.#handleData() } } else if (attribute === 'loading') { // Loading mode changed to Eager after attached so replace element. if (this.isConnected && oldVal !== 'eager' && this.loading === 'eager') { - handleData(this) + this.#handleData() } } } @@ -181,10 +137,10 @@ export default class IncludeFragmentElement extends HTMLElement { connectedCallback(): void { if (this.src && this.loading === 'eager') { - handleData(this) + this.#handleData() } if (this.loading === 'lazy') { - observer.observe(this) + this.#observer.observe(this) } } @@ -210,6 +166,51 @@ export default class IncludeFragmentElement extends HTMLElement { fetch(request: RequestInfo): Promise { return fetch(request) } + + #observer = new IntersectionObserver( + entries => { + for (const entry of entries) { + if (entry.isIntersecting) { + const {target} = entry + this.#observer.unobserve(target) + if (!(target instanceof IncludeFragmentElement)) return + if (target.loading === 'lazy') { + this.#handleData() + } + } + } + }, + { + // Currently the threshold is set to 256px from the bottom of the viewport + // with a threshold of 0.1. This means the element will not load until about + // 2 keyboard-down-arrow presses away from being visible in the viewport, + // giving us some time to fetch it before the contents are made visible + rootMargin: '0px 0px 256px 0px', + threshold: 0.01 + } + ) + + #handleData(): Promise { + this.#observer.unobserve(this) + + return getData(this).then( + (html: string) => { + const template = document.createElement('template') + // eslint-disable-next-line github/no-inner-html + template.innerHTML = html + const fragment = document.importNode(template.content, true) + const canceled = !this.dispatchEvent( + new CustomEvent('include-fragment-replace', {cancelable: true, detail: {fragment}}) + ) + if (canceled) return + this.replaceWith(fragment) + this.dispatchEvent(new CustomEvent('include-fragment-replaced')) + }, + () => { + this.classList.add('is-error') + } + ) + } } declare global { From 73421ea886f07f0a427c50779d83b1bdfa99a0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Wed, 30 Mar 2022 14:47:58 +0100 Subject: [PATCH 3/3] Move `getData` and `fetchDataWithEvents` into the component --- src/index.ts | 125 +++++++++++++++++++++++++-------------------------- 1 file changed, 62 insertions(+), 63 deletions(-) diff --git a/src/index.ts b/src/index.ts index 552e5bf..3561f24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,65 +9,6 @@ function isWildcard(accept: string | null) { return accept && !!accept.split(',').find(x => x.match(/^\s*\*\/\*/)) } -function getData(el: IncludeFragmentElement) { - const src = el.src - let data = privateData.get(el) - if (data && data.src === src) { - return data.data - } else { - if (src) { - data = fetchDataWithEvents(el) - } else { - data = Promise.reject(new Error('missing src')) - } - privateData.set(el, {src, data}) - return data - } -} - -function fetchDataWithEvents(el: IncludeFragmentElement) { - // We mimic the same event order as , including the spec - // which states events must be dispatched after "queue a task". - // https://www.w3.org/TR/html52/semantics-embedded-content.html#the-img-element - return task() - .then(() => { - el.dispatchEvent(new Event('loadstart')) - return el.fetch(el.request()) - }) - .then(response => { - if (response.status !== 200) { - throw new Error(`Failed to load resource: the server responded with a status of ${response.status}`) - } - const ct = response.headers.get('Content-Type') - if (!isWildcard(el.accept) && (!ct || !ct.includes(el.accept ? el.accept : 'text/html'))) { - throw new Error(`Failed to load resource: expected ${el.accept || 'text/html'} but was ${ct}`) - } - return response.text() - }) - .then( - data => { - // Dispatch `load` and `loadend` async to allow - // the `load()` promise to resolve _before_ these - // events are fired. - task().then(() => { - el.dispatchEvent(new Event('load')) - el.dispatchEvent(new Event('loadend')) - }) - return data - }, - error => { - // Dispatch `error` and `loadend` async to allow - // the `load()` promise to resolve _before_ these - // events are fired. - task().then(() => { - el.dispatchEvent(new Event('error')) - el.dispatchEvent(new Event('loadend')) - }) - throw error - } - ) -} - export default class IncludeFragmentElement extends HTMLElement { static get observedAttributes(): string[] { return ['src', 'loading'] @@ -106,7 +47,7 @@ export default class IncludeFragmentElement extends HTMLElement { } get data(): Promise { - return getData(this) + return this.#getData() } attributeChangedCallback(attribute: string, oldVal: string | null): void { @@ -160,7 +101,7 @@ export default class IncludeFragmentElement extends HTMLElement { } load(): Promise { - return getData(this) + return this.#getData() } fetch(request: RequestInfo): Promise { @@ -192,8 +133,7 @@ export default class IncludeFragmentElement extends HTMLElement { #handleData(): Promise { this.#observer.unobserve(this) - - return getData(this).then( + return this.#getData().then( (html: string) => { const template = document.createElement('template') // eslint-disable-next-line github/no-inner-html @@ -211,6 +151,65 @@ export default class IncludeFragmentElement extends HTMLElement { } ) } + + #getData(): Promise { + const src = this.src + let data = privateData.get(this) + if (data && data.src === src) { + return data.data + } else { + if (src) { + data = this.#fetchDataWithEvents() + } else { + data = Promise.reject(new Error('missing src')) + } + privateData.set(this, {src, data}) + return data + } + } + + #fetchDataWithEvents(): Promise { + // We mimic the same event order as , including the spec + // which states events must be dispatched after "queue a task". + // https://www.w3.org/TR/html52/semantics-embedded-content.html#the-img-element + return task() + .then(() => { + this.dispatchEvent(new Event('loadstart')) + return this.fetch(this.request()) + }) + .then(response => { + if (response.status !== 200) { + throw new Error(`Failed to load resource: the server responded with a status of ${response.status}`) + } + const ct = response.headers.get('Content-Type') + if (!isWildcard(this.accept) && (!ct || !ct.includes(this.accept ? this.accept : 'text/html'))) { + throw new Error(`Failed to load resource: expected ${this.accept || 'text/html'} but was ${ct}`) + } + return response.text() + }) + .then( + data => { + // Dispatch `load` and `loadend` async to allow + // the `load()` promise to resolve _before_ these + // events are fired. + task().then(() => { + this.dispatchEvent(new Event('load')) + this.dispatchEvent(new Event('loadend')) + }) + return data + }, + error => { + // Dispatch `error` and `loadend` async to allow + // the `load()` promise to resolve _before_ these + // events are fired. + task().then(() => { + this.dispatchEvent(new Event('error')) + this.dispatchEvent(new Event('loadend')) + }) + throw error + } + ) + } } declare global {