text-fragments-ts/src/ polyfill.ts
83 lines
3.7 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(window.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. if (documentFragmentDirective !== null) {
  19. const { documentIndicatedPart, ranges } = indicatedPartOfTheDocument_beginning({
  20. document,
  21. documentFragmentDirective,
  22. documentAllowTextFragmentDirective: true, // TEMP (TODO should be determined if possible)
  23. }) || undefined;
  24. if (documentIndicatedPart !== undefined) {
  25. scrollToTheFragment(documentIndicatedPart);
  26. }
  27. if (ranges !== undefined) {
  28. highlightRanges(ranges);
  29. }
  30. }
  31. }
  32. function pretendBrowserSupportsTextFragments(): void {
  33. const fragmentDirective: FragmentDirective = {};
  34. // Sneak in a note so one can discover whether the polyfill is used.
  35. Object.defineProperty(fragmentDirective, '_implementation', {
  36. value: 'text-fragments-ts',
  37. enumerable: false,
  38. });
  39. Object.defineProperty(window.location, 'fragmentDirective', {
  40. value: fragmentDirective,
  41. writable: false,
  42. });
  43. }
  44. // See § 3.6. Indicating The Text Match <https://wicg.github.io/scroll-to-text-fragment/#indicating-the-text-match>
  45. // 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.
  46. // 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.”
  47. // XXX How exactly could this be an attack vector?
  48. function highlightRanges(ranges: Range[]): void {
  49. const selection = window.getSelection() as Selection; // should be non-null on top window.
  50. selection.removeAllRanges();
  51. for (const range of ranges) {
  52. selection.addRange(range);
  53. }
  54. }
  55. function install(): void {
  56. // Do nothing if the browser already supports (text) fragment directives.
  57. if (browserSupportsTextFragments())
  58. return;
  59. pretendBrowserSupportsTextFragments();
  60. // Run when the page is ready.
  61. window.addEventListener('load', run);
  62. // Could we somehow avoid activating in cases where the browser would retain scroll position, e.g. on page reload or history navigation?
  63. // Run whenever the location’s fragment identifier is changed.
  64. window.addEventListener('hashchange', run);
  65. // 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)
  66. }
  67. install();