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:
2026-10-14 17:50:33 +00:00
parent 1471b9b9d0
commit 60007d4e6a
13 changed files with 191 additions and 193 deletions

View File

@@ -19,7 +19,7 @@ async function loadFeeds(keepSel?: boolean){
// brand new account) leaves S.feed alone, so the empty state's own message shows instead.
if(!keepSel && S.feeds.length){
const known = S.feed && (VIEWS[S.feed] || S.feeds.some(f=>f.id===S.feed));
selectFeed(known ? S.feed : ':all');
selectFeed(known ? S.feed! : ':all');
}
}
// An OPML can hold dozens of feeds; the ones with something new go first. sort is stable, so the
@@ -45,9 +45,9 @@ function setScanning(id: string, on: boolean){
paintScanning();
}
function paintScanning(){
const slow=(id: string)=>scanning.has(id)&&Date.now()-scanning.get(id)>=SPIN_AFTER;
const slow=(id: string)=>scanning.has(id)&&Date.now()-scanning.get(id)!>=SPIN_AFTER;
for(const row of $$('#feedlist .feed')){
const id=row.dataset.id;
const id=row.dataset.id!;
const on=slow(id)||S.feeds.some(c=>c.group===id&&slow(c.id));
row.classList.toggle('scanning',on);
if(on) row.title='Checking for new items…'; else row.removeAttribute('title');
@@ -73,21 +73,21 @@ function spinEnd(done?: {only: string|null, forced: boolean}){
}
function renderFeeds(){
const q=$('#feedFilter').value.trim().toLowerCase();
const q=$<HTMLInputElement>('#feedFilter').value.trim().toLowerCase();
const list=$('#feedlist'); const top=list.scrollTop;
// Every row is replaced, so put the keyboard back on the row, or the triangle, it was on.
const a=document.activeElement, was=a&&a.closest&&a.closest<HTMLElement>('#feedlist [data-id]');
const back=was&&([was.dataset.id,a.classList.contains('chev')] as [string, boolean]);
const done=()=>{
list.scrollTop=top;
const row=back&&list.querySelector(`[data-id="${CSS.escape(back[0])}"]`);
if(row) (back[1]&&$('.chev',row)||row).focus();
const row=back&&list.querySelector<HTMLElement>(`[data-id="${CSS.escape(back[0])}"]`);
if(row) (back[1]&&$('.chev',row)||row as HTMLElement).focus();
};
// The images already drawn, to go into the rows that replace theirs. Made anew, every icon in
// the list went blank and loaded again, once a frame through a check of every feed, as each
// feed's new row came in (#134). Matched by their markup, so a changed one is made anew.
const drawn=new Map<string,HTMLImageElement[]>();
for(const img of $$('img',list)){ const k=img.outerHTML; if(!drawn.has(k)) drawn.set(k,[]); drawn.get(k).push(img); }
for(const img of $$<HTMLImageElement>('img',list)){ const k=img.outerHTML; if(!drawn.has(k)) drawn.set(k,[]); drawn.get(k)!.push(img); }
list.innerHTML='';
const unreadAll=S.feeds.reduce((n,f)=>n+(f.unread||0),0);
const places=document.createElement('div');
@@ -168,7 +168,7 @@ function renderFeeds(){
done();
}
function selectFeed(id: string){
S.feed=id; S.offset=0; S.sel=null; S.q=''; $('#epSearch').value='';
S.feed=id; S.offset=0; S.sel=null; S.q=''; $<HTMLInputElement>('#epSearch').value='';
try{ localStorage.setItem('ipx.feed',id); }catch{}
renderFeeds(); renderFeed(); loadEntries();
}
@@ -185,6 +185,6 @@ function failBannerHTML(f: Feed){
}
function wireFailBanner(box: ParentNode,f: Feed){
const un=$('[data-ffail="unsub"]',box); if(un) un.onclick=()=>removeFeed(f);
const nu=$('[data-ffail="newurl"]',box); if(nu) nu.onclick=()=>settingsModal(f,f.failing.new_url);
const nu=$('[data-ffail="newurl"]',box); if(nu) nu.onclick=()=>settingsModal(f,f.failing?.new_url??undefined);
}