text-fragments-ts/src/ polyfill.ts
100 lines
4.4 KiB

  1. // A polyfill that makes the browser scroll to, and highlight, the Text Fragment given in the location’s fragment directive.
  2. // See https://wicg.github.io/scroll-to-text-fragment/
  3. // Based on the version of 12 August 2020. <https://raw.githubusercontent.com/WICG/scroll-to-text-fragment/60f5f63b4997bde7e688cacf897e1167c622e100/index.html>
  4. // This implementation assumes the browser has already performed the normal procedures to identify and scroll to the fragment, without support for Text Fragments.
  5. import {
  6. initializeDocumentFragmentDirective,
  7. indicatedPartOfTheDocument_beginning,
  8. scrollToTheFragment,
  9. FragmentDirective,
  10. browserSupportsTextFragments,
  11. } from './index.js';
  12. function run(): void {
  13. const { documentUrl, documentFragmentDirective } = initializeDocumentFragmentDirective(document) ?? {};
  14. if (documentUrl !== document.URL) {
  15. // We could change the location to hide the fragment directive from the fragment, as the spec prescribes; however this would also hide it from the user (and could trigger other event listeners).
  16. // document.location.replace(documentUrl);
  17. }
  18. applyFragmentDirective({ document, documentFragmentDirective });
  19. }
  20. function applyFragmentDirective({ document, documentFragmentDirective } : {
  21. document: Document,
  22. documentFragmentDirective: string | null,
  23. }): void {
  24. if (documentFragmentDirective !== null) {
  25. const { documentIndicatedPart, ranges } = indicatedPartOfTheDocument_beginning({
  26. document,
  27. documentFragmentDirective,
  28. documentAllowTextFragmentDirective: true, // TEMP (TODO should be determined if possible)
  29. }) || undefined;
  30. if (documentIndicatedPart !== undefined) {
  31. scrollToTheFragment(documentIndicatedPart);
  32. }
  33. if (ranges !== undefined) {
  34. highlightRanges(ranges);
  35. }
  36. }
  37. }
  38. function pretendBrowserSupportsTextFragments(): void {
  39. const fragmentDirective: FragmentDirective = {};
  40. // Sneak in a note so one can discover whether the polyfill is used.
  41. Object.defineProperty(fragmentDirective, '_implementation', {
  42. value: 'text-fragments-ts',
  43. enumerable: false,
  44. });
  45. Object.defineProperty(document, 'fragmentDirective', {
  46. value: fragmentDirective,
  47. writable: false,
  48. });
  49. }
  50. // See § 3.6. Indicating The Text Match <https://wicg.github.io/scroll-to-text-fragment/#indicating-the-text-match>
  51. // This implements a simple method to highlight the indicated ranges, without modifying the DOM: we use the window’s selection. This has the limitation that it disappears as soon as the user clicks anywhere; but the ability to dismiss it is a feature too; and it helps convey that the highlight is not part of the page itself.
  52. // Note the spec urges against this approach: “the UA must not use the Document’s selection to indicate the text match as doing so could allow attack vectors for content exfiltration.”
  53. // XXX How exactly could this be an attack vector?
  54. function highlightRanges(ranges: Range[]): void {
  55. const selection = window.getSelection() as Selection; // should be non-null on top window.
  56. selection.removeAllRanges();
  57. for (const range of ranges) {
  58. selection.addRange(range);
  59. }
  60. }
  61. function install(): void {
  62. // Do nothing if the browser already supports (text) fragment directives.
  63. if (browserSupportsTextFragments())
  64. return;
  65. pretendBrowserSupportsTextFragments();
  66. // Run when the page is ready.
  67. window.addEventListener('load', run);
  68. // Could we somehow avoid activating in cases where the browser would retain scroll position, e.g. on page reload or history navigation?
  69. // Run whenever the location’s fragment identifier is changed.
  70. window.addEventListener('hashchange', run);
  71. // Could we somehow also detect it when the user navigates to exactly the same fragment again? (to mimic browser/Firefox’s behaviour when just pressing enter in the URL bar)
  72. }
  73. install();
  74. // A small tool to use from e.g. the browser console.
  75. export function applyFragDir(fragmentDirective: string) {
  76. if (typeof fragmentDirective !== 'string' || !fragmentDirective.includes(':~:'))
  77. throw new TypeError('Expected a fragment directive string, e.g. ":~:text=bla&text=blub"');
  78. fragmentDirective = fragmentDirective.substring(fragmentDirective.indexOf(':~:') + 3);
  79. applyFragmentDirective({
  80. document,
  81. documentFragmentDirective: fragmentDirective,
  82. });
  83. }