Thread viewer for Bluesky

redesigned how DOM types are handled for TS/JSDoc

+14 -14
embed_component.js
··· 10 10 this.embed = embed; 11 11 } 12 12 13 - /** @returns {AnyElement} */ 13 + /** @returns {HTMLElement} */ 14 14 15 15 buildElement() { 16 16 if (this.embed instanceof RawRecordEmbed) { ··· 54 54 } 55 55 } 56 56 57 - /** @returns {AnyElement} */ 57 + /** @returns {HTMLElement} */ 58 58 59 59 quotedPostPlaceholder() { 60 60 return $tag('div.quote-embed', { ··· 62 62 }); 63 63 } 64 64 65 - /** @param {InlineRecordEmbed | InlineRecordWithMediaEmbed} embed, @returns {AnyElement} */ 65 + /** @param {InlineRecordEmbed | InlineRecordWithMediaEmbed} embed, @returns {HTMLElement} */ 66 66 67 67 buildQuotedPostElement(embed) { 68 68 let div = $tag('div.quote-embed'); ··· 88 88 return div; 89 89 } 90 90 91 - /** @params {RawLinkEmbed | InlineLinkEmbed} embed, @returns {AnyElement} */ 91 + /** @params {RawLinkEmbed | InlineLinkEmbed} embed, @returns {HTMLElement} */ 92 92 93 93 buildLinkComponent(embed) { 94 94 let hostname; ··· 128 128 return a; 129 129 } 130 130 131 - /** @param {FeedGeneratorRecord} feedgen, @returns {AnyElement} */ 131 + /** @param {FeedGeneratorRecord} feedgen, @returns {HTMLElement} */ 132 132 133 133 buildFeedGeneratorView(feedgen) { 134 134 let link = this.linkToFeedGenerator(feedgen); ··· 137 137 let box = $tag('div'); 138 138 139 139 if (feedgen.avatar) { 140 - let avatar = $tag('img.avatar'); 140 + let avatar = $tag('img.avatar', HTMLImageElement); 141 141 avatar.src = feedgen.avatar; 142 142 box.append(avatar); 143 143 } ··· 167 167 return `https://bsky.app/profile/${repo}/feed/${rkey}`; 168 168 } 169 169 170 - /** @param {UserListRecord} list, @returns {AnyElement} */ 170 + /** @param {UserListRecord} list, @returns {HTMLElement} */ 171 171 172 172 buildUserListView(list) { 173 173 let link = this.linkToUserList(list); ··· 176 176 let box = $tag('div'); 177 177 178 178 if (list.avatar) { 179 - let avatar = $tag('img.avatar'); 179 + let avatar = $tag('img.avatar', HTMLImageElement); 180 180 avatar.src = list.avatar; 181 181 box.append(avatar); 182 182 } ··· 207 207 return a; 208 208 } 209 209 210 - /** @param {StarterPackRecord} pack, @returns {AnyElement} */ 210 + /** @param {StarterPackRecord} pack, @returns {HTMLElement} */ 211 211 212 212 buildStarterPackView(pack) { 213 213 let { repo, rkey } = atURI(pack.uri); ··· 236 236 return `https://bsky.app/profile/${repo}/lists/${rkey}`; 237 237 } 238 238 239 - /** @params {RawImageEmbed | InlineImageEmbed} embed, @returns {AnyElement} */ 239 + /** @params {RawImageEmbed | InlineImageEmbed} embed, @returns {HTMLElement} */ 240 240 241 241 buildImagesComponent(embed) { 242 242 let wrapper = $tag('div'); ··· 246 246 p.append('['); 247 247 248 248 // TODO: load image 249 - let a = $tag('a', { text: "Image" }); 249 + let a = $tag('a', { text: "Image" }, HTMLLinkElement); 250 250 251 251 if (image.fullsize) { 252 252 a.href = image.fullsize; ··· 272 272 return wrapper; 273 273 } 274 274 275 - /** @params {RawVideoEmbed | InlineVideoEmbed} embed, @returns {AnyElement} */ 275 + /** @params {RawVideoEmbed | InlineVideoEmbed} embed, @returns {HTMLElement} */ 276 276 277 277 buildVideoComponent(embed) { 278 278 let wrapper = $tag('div'); 279 279 280 280 // TODO: load thumbnail 281 - let a = $tag('a', { text: "Video" }); 281 + let a = $tag('a', { text: "Video" }, HTMLLinkElement); 282 282 283 283 if (embed.playlistURL) { 284 284 a.href = embed.playlistURL; ··· 303 303 return wrapper; 304 304 } 305 305 306 - /** @param {string} uri, @param {AnyElement} div, @returns Promise<void> */ 306 + /** @param {string} uri, @param {HTMLElement} div, @returns Promise<void> */ 307 307 308 308 async loadQuotedPost(uri, div) { 309 309 let record = await api.loadPostIfExists(uri);
+31 -27
post_component.js
··· 5 5 class PostComponent { 6 6 /** 7 7 * Post component's root HTML element, if built. 8 - * @type {AnyElement | undefined} 8 + * @type {HTMLElement | undefined} 9 9 */ 10 10 _rootElement; 11 11 ··· 26 26 } 27 27 28 28 /** 29 - * @returns {AnyElement} 29 + * @returns {HTMLElement} 30 30 */ 31 31 get rootElement() { 32 32 if (!this._rootElement) { ··· 91 91 } 92 92 } 93 93 94 - /** @param {AnyElement} nodeToUpdate */ 94 + /** @param {HTMLElement} nodeToUpdate */ 95 95 installIntoElement(nodeToUpdate) { 96 96 let view = this.buildElement(); 97 97 98 - nodeToUpdate.querySelector('.content').replaceWith(view.querySelector('.content')); 98 + let oldContent = $(nodeToUpdate.querySelector('.content')); 99 + let newContent = $(view.querySelector('.content')); 100 + oldContent.replaceWith(newContent); 101 + 99 102 this._rootElement = nodeToUpdate; 100 103 } 101 104 102 - /** @returns {AnyElement} */ 105 + /** @returns {HTMLElement} */ 103 106 buildElement() { 104 107 if (this._rootElement) { 105 108 return this._rootElement; ··· 196 199 return div; 197 200 } 198 201 199 - /** @returns {AnyElement} */ 202 + /** @returns {HTMLElement} */ 200 203 201 204 buildPostHeader() { 202 205 let timeFormat = this.timeFormatForTimestamp; ··· 260 263 /** @param {string} url, @returns {HTMLImageElement} */ 261 264 262 265 buildUserAvatar(url) { 263 - let avatar = $tag('img.avatar', { loading: 'lazy' }); // needs to be set before src! 266 + let avatar = $tag('img.avatar', { loading: 'lazy' }, HTMLImageElement); // needs to be set before src! 264 267 avatar.src = url; 265 268 window.avatarPreloader.observe(avatar); 266 269 return avatar; 267 270 } 268 271 269 - /** @returns {AnyElement} */ 272 + /** @returns {HTMLElement} */ 270 273 271 274 buildPostBody() { 272 275 if (this.post.originalFediContent) { ··· 297 300 return p; 298 301 } 299 302 300 - /** @param {string[]} tags, @returns {AnyElement} */ 303 + /** @param {string[]} tags, @returns {HTMLElement} */ 301 304 302 305 buildTagsRow(tags) { 303 306 let p = $tag('p.tags'); ··· 313 316 return p; 314 317 } 315 318 316 - /** @returns {AnyElement} */ 319 + /** @returns {HTMLElement} */ 317 320 318 321 buildStatsFooter() { 319 322 let stats = $tag('p.stats'); ··· 347 350 return stats; 348 351 } 349 352 350 - /** @param {number} count, @param {boolean} expanded, @returns {AnyElement} */ 353 + /** @param {number} count, @param {boolean} expanded, @returns {HTMLElement} */ 351 354 352 355 buildQuotesIconLink(count, expanded) { 353 356 let q = new URL(getLocation()); ··· 369 372 /** @param {number} quoteCount, @param {boolean} expanded */ 370 373 371 374 appendQuotesIconLink(quoteCount, expanded) { 372 - let stats = this.rootElement.querySelector(':scope > .content > p.stats'); 375 + let stats = $(this.rootElement.querySelector(':scope > .content > p.stats')); 373 376 let quotesLink = this.buildQuotesIconLink(quoteCount, expanded); 374 377 stats.append(quotesLink); 375 378 } 376 379 377 - /** @returns {AnyElement} */ 380 + /** @returns {HTMLElement} */ 378 381 379 382 buildLoadMoreLink() { 380 383 let loadMore = $tag('p'); ··· 394 397 return loadMore; 395 398 } 396 399 397 - /** @returns {AnyElement} */ 400 + /** @returns {HTMLElement} */ 398 401 399 402 buildHiddenRepliesLink() { 400 403 let loadMore = $tag('p.hidden-replies'); ··· 419 422 return loadMore; 420 423 } 421 424 422 - /** @param {HTMLLinkElement} loadMoreButton */ 425 + /** @param {HTMLElement} loadMoreButton */ 423 426 424 427 loadHiddenReplies(loadMoreButton) { 425 428 loadMoreButton.innerHTML = `<img class="loader" src="icons/sunny.png">`; ··· 443 446 }); 444 447 } 445 448 446 - /** @param {AnyElement} div, @returns {AnyElement} */ 449 + /** @param {HTMLElement} div, @returns {HTMLElement} */ 447 450 448 451 buildBlockedPostElement(div) { 449 452 let p = $tag('p.blocked-header'); ··· 458 461 let blockStatus = this.post.blockedByUser ? 'has blocked you' : this.post.blocksUser ? "you've blocked them" : ''; 459 462 blockStatus = blockStatus ? `, ${blockStatus}` : ''; 460 463 461 - let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' }); 464 + let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' }, HTMLLinkElement); 462 465 p.append(' (', authorLink, blockStatus, ') '); 463 466 div.appendChild(p); 464 467 ··· 479 482 return div; 480 483 } 481 484 482 - /** @param {AnyElement} div, @returns {AnyElement} */ 485 + /** @param {HTMLElement} div, @returns {HTMLElement} */ 483 486 484 487 buildDetachedQuoteElement(div) { 485 488 let p = $tag('p.blocked-header'); ··· 491 494 return p; 492 495 } 493 496 494 - let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' }); 497 + let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' }, HTMLLinkElement); 495 498 p.append(' (', authorLink, ') '); 496 499 div.appendChild(p); 497 500 ··· 512 515 return div; 513 516 } 514 517 515 - /** @param {AnyElement} div, @returns {AnyElement} */ 518 + /** @param {HTMLElement} div, @returns {HTMLElement} */ 516 519 517 520 buildMissingPostElement(div) { 518 521 let p = $tag('p.blocked-header'); 519 522 p.innerHTML = `<i class="fa-solid fa-ban"></i> <span>Deleted post</span>`; 520 523 521 - let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' }); 524 + let authorLink = $tag('a', { href: this.didLinkToAuthor, target: '_blank', text: 'see author' }, HTMLLinkElement); 522 525 p.append(' (', authorLink, ') '); 523 526 524 527 this.loadReferencedPostAuthor(authorLink); ··· 528 531 return div; 529 532 } 530 533 531 - /** @param {string} uri, @param {AnyElement} div, @returns Promise<void> */ 534 + /** @param {string} uri, @param {HTMLElement} div, @returns Promise<void> */ 532 535 533 536 async loadBlockedPost(uri, div) { 534 537 let record = await appView.loadPostIfExists(this.post.uri); ··· 554 557 html: `<i class="fa-solid fa-arrows-split-up-and-left fa-rotate-180"></i>` 555 558 }); 556 559 557 - let header = div.querySelector('p.blocked-header'); 560 + let header = $(div.querySelector('p.blocked-header')); 558 561 let separator = $tag('span.separator', { html: '&bull;' }); 559 562 header.append(separator, ' ', a); 560 563 } 561 564 562 - div.querySelector('p.load-post').remove(); 565 + let loadPost = $(div.querySelector('p.load-post')); 566 + loadPost.remove(); 563 567 564 568 if (this.isRoot && this.post.parentReference) { 565 569 let { repo, rkey } = atURI(this.post.parentReference.uri); ··· 590 594 } 591 595 592 596 toggleSectionFold() { 593 - let plus = this.rootElement.querySelector(':scope > .margin .plus'); 597 + let plus = $(this.rootElement.querySelector(':scope > .margin .plus'), HTMLImageElement); 594 598 595 599 if (this.isCollapsed()) { 596 600 this.rootElement.classList.remove('collapsed'); ··· 601 605 } 602 606 } 603 607 604 - /** @param {AnyElement} heart */ 608 + /** @param {HTMLElement} heart */ 605 609 606 610 onHeartClick(heart) { 607 611 if (!this.post.hasViewerInfo) { ··· 627 631 return; 628 632 } 629 633 630 - let count = heart.nextElementSibling; 634 + let count = $(heart.nextElementSibling); 631 635 632 636 if (!heart.classList.contains('liked')) { 633 637 accountAPI.likePost(this.post).then((like) => {
+45 -37
skythread.js
··· 1 1 function init() { 2 - let document = /** @type {AnyElement} */ (/** @type {unknown} */ (window.document)); 3 - let html = /** @type {AnyElement} */ (/** @type {unknown} */ (window.document.body.parentNode)); 2 + let html = $(document.body.parentNode); 4 3 5 4 window.dateLocale = localStorage.getItem('locale') || undefined; 6 5 window.isIncognito = !!localStorage.getItem('incognito'); 7 6 window.biohazardEnabled = JSON.parse(localStorage.getItem('biohazard') ?? 'null'); 8 7 9 - window.loginDialog = document.querySelector('#login'); 10 - window.accountMenu = document.querySelector('#account_menu'); 8 + window.loginDialog = $(document.querySelector('#login')); 9 + window.accountMenu = $(document.querySelector('#account_menu')); 11 10 12 11 window.avatarPreloader = buildAvatarPreloader(); 13 12 ··· 15 14 $id('account_menu').style.visibility = 'hidden'; 16 15 }); 17 16 18 - document.querySelector('#search form').addEventListener('submit', (e) => { 17 + $(document.querySelector('#search form')).addEventListener('submit', (e) => { 19 18 e.preventDefault(); 20 19 submitSearch(); 21 20 }); ··· 34 33 }); 35 34 } 36 35 37 - document.querySelector('#login .info a').addEventListener('click', (e) => { 36 + $(document.querySelector('#login .info a')).addEventListener('click', (e) => { 38 37 e.preventDefault(); 39 38 toggleLoginInfo(); 40 39 }); 41 40 42 - document.querySelector('#login form').addEventListener('submit', (e) => { 41 + $(document.querySelector('#login form')).addEventListener('submit', (e) => { 43 42 e.preventDefault(); 44 43 submitLogin(); 45 44 }); 46 45 47 - document.querySelector('#biohazard_show').addEventListener('click', (e) => { 46 + $(document.querySelector('#biohazard_show')).addEventListener('click', (e) => { 48 47 e.preventDefault(); 49 48 50 49 window.biohazardEnabled = true; ··· 55 54 window.loadInfohazard = undefined; 56 55 } 57 56 58 - let target = /** @type {AnyElement} */ (/** @type {unknown} */ (e.target)); 57 + let target = $(e.target); 59 58 60 59 hideDialog(target.closest('.dialog')); 61 60 }); 62 61 63 - document.querySelector('#biohazard_hide').addEventListener('click', (e) => { 62 + $(document.querySelector('#biohazard_hide')).addEventListener('click', (e) => { 64 63 e.preventDefault(); 65 64 66 65 window.biohazardEnabled = false; ··· 68 67 toggleMenuButton('biohazard', false); 69 68 70 69 for (let p of document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post')) { 71 - p.style.display = 'none'; 70 + $(p).style.display = 'none'; 72 71 } 73 72 74 - let target = /** @type {AnyElement} */ (/** @type {unknown} */ (e.target)); 73 + let target = $(e.target); 75 74 76 75 hideDialog(target.closest('.dialog')); 77 76 }); 78 77 79 - document.querySelector('#account').addEventListener('click', (e) => { 78 + $(document.querySelector('#account')).addEventListener('click', (e) => { 80 79 toggleAccountMenu(); 81 80 e.stopPropagation(); 82 81 }); ··· 85 84 e.stopPropagation(); 86 85 }); 87 86 88 - accountMenu.querySelector('a[data-action=biohazard]').addEventListener('click', (e) => { 87 + $(accountMenu.querySelector('a[data-action=biohazard]')).addEventListener('click', (e) => { 89 88 e.preventDefault(); 90 89 91 90 let hazards = document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post'); ··· 94 93 window.biohazardEnabled = true; 95 94 localStorage.setItem('biohazard', 'true'); 96 95 toggleMenuButton('biohazard', true); 97 - Array.from(hazards).forEach(p => { p.style.display = 'block' }); 96 + Array.from(hazards).forEach(p => { $(p).style.display = 'block' }); 98 97 } else { 99 98 window.biohazardEnabled = false; 100 99 localStorage.setItem('biohazard', 'false'); 101 100 toggleMenuButton('biohazard', false); 102 - Array.from(hazards).forEach(p => { p.style.display = 'none' }); 101 + Array.from(hazards).forEach(p => { $(p).style.display = 'none' }); 103 102 } 104 103 }); 105 104 106 - accountMenu.querySelector('a[data-action=incognito]').addEventListener('click', (e) => { 105 + $(accountMenu.querySelector('a[data-action=incognito]')).addEventListener('click', (e) => { 107 106 e.preventDefault(); 108 107 109 108 if (isIncognito) { ··· 115 114 location.reload(); 116 115 }); 117 116 118 - accountMenu.querySelector('a[data-action=login]').addEventListener('click', (e) => { 117 + $(accountMenu.querySelector('a[data-action=login]')).addEventListener('click', (e) => { 119 118 e.preventDefault(); 120 119 toggleDialog(loginDialog); 121 120 $id('account_menu').style.visibility = 'hidden'; 122 121 }); 123 122 124 - accountMenu.querySelector('a[data-action=logout]').addEventListener('click', (e) => { 123 + $(accountMenu.querySelector('a[data-action=logout]')).addEventListener('click', (e) => { 125 124 e.preventDefault(); 126 125 logOut(); 127 126 }); ··· 181 180 } 182 181 } 183 182 184 - /** @param {AnyPost} post, @returns {AnyElement} */ 183 + /** @param {AnyPost} post, @returns {HTMLElement} */ 185 184 186 185 function buildParentLink(post) { 187 186 let p = $tag('p.back'); ··· 189 188 if (post instanceof BlockedPost) { 190 189 let element = new PostComponent(post, 'parent').buildElement(); 191 190 element.className = 'back'; 192 - element.querySelector('p.blocked-header span').innerText = 'Parent post blocked'; 191 + let span = $(element.querySelector('p.blocked-header span')); 192 + span.innerText = 'Parent post blocked'; 193 193 return element; 194 194 } else if (post instanceof MissingPost) { 195 195 p.innerHTML = `<i class="fa-solid fa-ban"></i> parent post has been deleted`; ··· 226 226 } 227 227 228 228 function showSearch() { 229 - $id('search').style.visibility = 'visible'; 230 - $id('search').querySelector('input[type=text]').focus(); 229 + let search = $id('search'); 230 + let searchField = $(search.querySelector('input[type=text]')); 231 + 232 + search.style.visibility = 'visible'; 233 + searchField.focus(); 231 234 } 232 235 233 236 function hideSearch() { ··· 271 274 /** @param {string} buttonName */ 272 275 273 276 function showMenuButton(buttonName) { 274 - let button = accountMenu.querySelector(`a[data-action=${buttonName}]`); 275 - button.parentNode.style.display = 'list-item'; 277 + let button = $(accountMenu.querySelector(`a[data-action=${buttonName}]`)); 278 + let item = $(button.parentNode); 279 + item.style.display = 'list-item'; 276 280 } 277 281 278 282 /** @param {string} buttonName */ 279 283 280 284 function hideMenuButton(buttonName) { 281 - let button = accountMenu.querySelector(`a[data-action=${buttonName}]`); 282 - button.parentNode.style.display = 'none'; 285 + let button = $(accountMenu.querySelector(`a[data-action=${buttonName}]`)); 286 + let item = $(button.parentNode); 287 + item.style.display = 'none'; 283 288 } 284 289 285 290 /** @param {string} buttonName, @param {boolean} state */ 286 291 287 292 function toggleMenuButton(buttonName, state) { 288 - let button = accountMenu.querySelector(`a[data-action=${buttonName}]`); 289 - button.querySelector('.check').style.display = (state) ? 'inline' : 'none'; 293 + let button = $(accountMenu.querySelector(`a[data-action=${buttonName}]`)); 294 + let check = $(button.querySelector('.check')); 295 + check.style.display = (state) ? 'inline' : 'none'; 290 296 } 291 297 292 298 /** @param {boolean | 'incognito'} loggedIn, @param {string | undefined | null} [avatar] */ ··· 295 301 let account = $id('account'); 296 302 297 303 if (loggedIn === true && avatar) { 298 - let button = account.querySelector('i'); 304 + let button = $(account.querySelector('i')); 299 305 300 306 let img = $tag('img.avatar', { src: avatar }); 301 307 img.style.display = 'none'; ··· 318 324 } 319 325 320 326 function submitLogin() { 321 - let handle = $id('login_handle'); 322 - let password = $id('login_password'); 327 + let handle = $id('login_handle', HTMLInputElement); 328 + let password = $id('login_password', HTMLInputElement); 323 329 let submit = $id('login_submit'); 324 330 let cloudy = $id('cloudy'); 325 331 ··· 401 407 } 402 408 403 409 function submitSearch() { 404 - let url = $id('search').querySelector('input[name=q]').value.trim(); 410 + let search = $id('search'); 411 + let searchField = $(search.querySelector('input[name=q]'), HTMLInputElement); 412 + let url = searchField.value.trim(); 405 413 406 414 if (!url) { return } 407 415 ··· 702 710 }); 703 711 } 704 712 705 - /** @param {Post} post, @param {AnyElement} nodeToUpdate */ 713 + /** @param {Post} post, @param {HTMLElement} nodeToUpdate */ 706 714 707 715 function loadSubtree(post, nodeToUpdate) { 708 716 api.loadThreadByAtURI(post.uri).then(json => { ··· 715 723 }).catch(showError); 716 724 } 717 725 718 - /** @param {Post} post, @param {AnyElement} nodeToUpdate */ 726 + /** @param {Post} post, @param {HTMLElement} nodeToUpdate */ 719 727 720 728 function loadHiddenSubtree(post, nodeToUpdate) { 721 - let content = nodeToUpdate.querySelector('.content'); 722 - let hiddenRepliesDiv = content.querySelector(':scope > .hidden-replies'); 729 + let content = $(nodeToUpdate.querySelector('.content')); 730 + let hiddenRepliesDiv = $(content.querySelector(':scope > .hidden-replies')); 723 731 724 732 blueAPI.getReplies(post.uri).then(replies => { 725 733 let missingReplies = replies.filter(r => !post.replies.some(x => x.uri === r));
+10 -25
types.d.ts
··· 11 11 declare var api: BlueskyAPI; 12 12 declare var isIncognito: boolean; 13 13 declare var biohazardEnabled: boolean; 14 - declare var loginDialog: AnyElement; 15 - declare var accountMenu: AnyElement; 14 + declare var loginDialog: HTMLElement; 15 + declare var accountMenu: HTMLElement; 16 16 declare var avatarPreloader: IntersectionObserver; 17 17 18 - type SomeElement = Element | HTMLElement | AnyElement; 19 18 type json = Record<string, any>; 20 19 21 - interface AnyElement { 22 - classList: CSSClassList; 23 - className: string; 24 - innerText: string; 25 - innerHTML: string; 26 - nextElementSibling: AnyElement; 27 - parentNode: AnyElement; 28 - src: string; 29 - style: CSSStyleDeclaration; 20 + function $tag(tag: string): HTMLElement; 21 + function $tag<T>(tag: string, type: new (...args: any[]) => T): T; 22 + function $tag(tag: string, params: string | object): HTMLElement; 23 + function $tag<T>(tag: string, params: string | object, type: new (...args: any[]) => T): T; 30 24 31 - addEventListener<K extends keyof DocumentEventMap>( 32 - type: K, listener: EventListenerOrEventListenerObject 33 - ): void; 25 + function $id(id: string): HTMLElement; 26 + function $id<T>(id: string, type: new (...args: any[]) => T): T; 34 27 35 - append(...e: Array<string | SomeElement>): void; 36 - appendChild(e: SomeElement): void; 37 - closest(q: string): AnyElement; 38 - querySelector(q: string): AnyElement; 39 - querySelectorAll(q: string): AnyElement[]; 40 - prepend(...e: Array<string | SomeElement>): void; 41 - remove(): void; 42 - replaceChildren(e: SomeElement): void; 43 - replaceWith(e: SomeElement): void; 44 - } 28 + function $(element: Node | EventTarget | null): HTMLElement; 29 + function $<T>(element: Node | EventTarget | null, type: new (...args: any[]) => T): T;
+21 -6
utils.js
··· 17 17 } 18 18 } 19 19 20 - /** @param {string} tag, @param {string | object} [params], @returns {any} */ 20 + /** 21 + * @template T 22 + * @param {string} tag 23 + * @param {string | object} params 24 + * @param {new (...args: any[]) => T} type 25 + * @returns {T} 26 + */ 21 27 22 - function $tag(tag, params) { 28 + function $tag(tag, params, type) { 23 29 let element; 24 30 let parts = tag.split('.'); 25 31 ··· 45 51 } 46 52 } 47 53 48 - return element; 54 + return /** @type {T} */ (element); 55 + } 56 + 57 + function $id(name, type) { 58 + return (document.getElementById(name)); 49 59 } 50 60 51 - /** @param {string} name, @returns {any} */ 61 + /** 62 + * @template T 63 + * @param {Node | EventTarget | null} element 64 + * @param {new (...args: any[]) => T} type 65 + * @returns {T} 66 + */ 52 67 53 - function $id(name) { 54 - return document.getElementById(name); 68 + function $(element, type) { 69 + return /** @type {T} */ (element); 55 70 } 56 71 57 72 /** @param {string} uri, @returns {AtURI} */