Page type-check: strict throughout, a typed $, null checks (#236)
$<T> returns the element type it is given and assumes the element is there: 122 lookups take their type from the tag their id is on in the markup. A handler reads its target through a cast, since a typed element will not take a handler whose event names a narrower target. strictNullChecks found no bug the page had; its 94 places were values the code already handles in ways the type-check could not follow (data- attributes the page writes, a map entry set the line before, Share drawn only for an item with a link), each now said so. isPlayable stays a plain boolean: as a type guard its false would have meant "no file", and it is also false for a file that is not audio or video. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
// `any`: the page reads .value, .dataset and .onclick off whatever it looks up, and the
|
||||
// smoke test, not the type checker, is what makes sure a selector exists.
|
||||
const $ = (s: string, r: ParentNode = document): any => r.querySelector(s);
|
||||
const $$ = (s: string, r: ParentNode = document): any[] => [...r.querySelectorAll(s)];
|
||||
const $ = <T extends HTMLElement = HTMLElement>(s: string, r: ParentNode = document): T => r.querySelector<T>(s)!;
|
||||
const $$ = <T extends HTMLElement = HTMLElement>(s: string, r: ParentNode = document): T[] => [...r.querySelectorAll<T>(s)];
|
||||
/// What went wrong, from whatever a catch caught: the page throws Errors, but a catch cannot know.
|
||||
const why = (e: unknown): string => e instanceof Error ? e.message : String(e);
|
||||
const ESCAPED: Record<string,string> = {'&':'&','<':'<','>':'>','"':'"',"'":'''};
|
||||
@@ -61,7 +61,7 @@ const EQ='<span class="eq" aria-hidden="true"><i></i><i></i><i></i></span>';
|
||||
// One meaning per icon: minus unsubscribes, x closes or cancels, plus adds or subscribes, and a
|
||||
// dialog's confirm button carries the icon of what it does. Words go in the tooltip.
|
||||
// The page's own buttons name their icon; this draws it in, ahead of any label they carry.
|
||||
for(const b of $$('[data-icon]')) b.insertAdjacentHTML('afterbegin',ICON[b.dataset.icon]);
|
||||
for(const b of $$('[data-icon]')) b.insertAdjacentHTML('afterbegin',ICON[b.dataset.icon!]);
|
||||
|
||||
/// An episode whose DeadAir copy is still being made: it plays as it is until then, and the line
|
||||
/// becomes how much shorter it is once the copy is ready (the dead_air event).
|
||||
@@ -198,7 +198,7 @@ function tileHTML(name: string,cls?: string){
|
||||
function artSrc(url: string){
|
||||
return /^https?:\/\//i.test(url) ? '/api/art?u='+encodeURIComponent(url) : url;
|
||||
}
|
||||
function artHTML(url: string | null, name: string, cls?: string){
|
||||
function artHTML(url: string | null | undefined, name: string, cls?: string){
|
||||
return url
|
||||
? `<img class="art ${cls||''}" src="${esc(artSrc(url))}" alt="" loading="lazy" onerror="this.outerHTML=${esc(JSON.stringify(tileHTML(name,cls)))}">`
|
||||
: tileHTML(name,cls);
|
||||
@@ -209,7 +209,7 @@ function folderArt(f: Feed,kids: Feed[]){
|
||||
if(art.length<4) return artHTML(f.image,f.title||f.id);
|
||||
// Tinted underneath, so art that fails to load leaves colour behind rather than a hole.
|
||||
return `<div class="art ini mosaic" style="--tint:${tint(f.title||f.id)}">${art.map(c=>
|
||||
`<img src="${esc(artSrc(c.image))}" alt="" loading="lazy" onerror="this.style.visibility='hidden'">`).join('')}</div>`;
|
||||
`<img src="${esc(artSrc(c.image!))}" alt="" loading="lazy" onerror="this.style.visibility='hidden'">`).join('')}</div>`;
|
||||
}
|
||||
|
||||
/// The sidebar slides over the page on a phone, so it needs a scrim to tap away.
|
||||
@@ -225,7 +225,7 @@ const S: State = {
|
||||
filter:(()=>{ try{ return localStorage.getItem('ipx.filter'); }catch{ return null; } })()||'all',
|
||||
q:'', sel:null, me:null,
|
||||
// The item table's order, kept across visits. The server sorts: a list arrives fifty at a time.
|
||||
sort:(()=>{ try{ return JSON.parse(localStorage.getItem('ipx.sort')); }catch{ return null; } })()
|
||||
sort:(()=>{ try{ return JSON.parse(localStorage.getItem('ipx.sort')??'null'); }catch{ return null; } })()
|
||||
||{col:'published',dir:'desc'},
|
||||
};
|
||||
const LIMIT = 50;
|
||||
|
||||
Reference in New Issue
Block a user