text-fragments-ts/lib/ polyfill.js
99 lines
5.3 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 { initializeDocumentFragmentDirective, indicatedPartOfTheDocument_beginning, scrollToTheFragment, browserSupportsTextFragments, } from './index.js';
  6. function run() {
  7. var _a;
  8. const { documentUrl, documentFragmentDirective } = (_a = initializeDocumentFragmentDirective(document)) !== null && _a !== void 0 ? _a : {};
  9. if (documentUrl !== document.URL) {
  10. // 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).
  11. // document.location.replace(documentUrl);
  12. }
  13. applyFragmentDirective({ document, documentFragmentDirective });
  14. }
  15. function applyFragmentDirective({ document, documentFragmentDirective }) {
  16. if (documentFragmentDirective !== null) {
  17. const { documentIndicatedPart, ranges } = indicatedPartOfTheDocument_beginning({
  18. document,
  19. documentFragmentDirective,
  20. documentAllowTextFragmentDirective: true,
  21. }) || undefined;
  22. if (documentIndicatedPart !== undefined) {
  23. scrollToTheFragment(documentIndicatedPart);
  24. }
  25. if (ranges !== undefined) {
  26. highlightRanges(ranges);
  27. }
  28. }
  29. }
  30. function pretendBrowserSupportsTextFragments() {
  31. const fragmentDirective = {};
  32. // Sneak in a note so one can discover whether the polyfill is used.
  33. Object.defineProperty(fragmentDirective, '_implementation', {
  34. value: 'text-fragments-ts',
  35. enumerable: false,
  36. });
  37. Object.defineProperty(document, 'fragmentDirective', {
  38. value: fragmentDirective,
  39. writable: false,
  40. });
  41. }
  42. // See § 3.6. Indicating The Text Match <https://wicg.github.io/scroll-to-text-fragment/#indicating-the-text-match>
  43. // 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.
  44. // 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.”
  45. // XXX How exactly could this be an attack vector?
  46. function highlightRanges(ranges) {
  47. const selection = window.getSelection(); // should be non-null on top window.
  48. selection.removeAllRanges();
  49. for (const range of ranges) {
  50. selection.addRange(range);
  51. }
  52. }
  53. // Listen to link clicks, and activate the polyfill when a link points to a fragment within the same page.
  54. function addLinkClickListeners() {
  55. const linkElements = [
  56. ...document.getElementsByTagName('a'),
  57. ...document.getElementsByTagName('area'),
  58. ];
  59. linkElements.forEach(element => {
  60. element.addEventListener('click', () => {
  61. if (element.href.split('#')[0] === document.URL.split('#')[0]) {
  62. const fragId = element.href.split('#')[1];
  63. if (fragId && fragId.includes(':~:')) {
  64. const fragmentDirective = fragId.substring(fragId.indexOf(':~:') + 3);
  65. applyFragmentDirective({
  66. document,
  67. documentFragmentDirective: fragmentDirective,
  68. });
  69. }
  70. }
  71. });
  72. });
  73. }
  74. function install() {
  75. if (browserSupportsTextFragments()) {
  76. // Chromium’s implementation currently does not trigger when clicking links pointing to quotes within the same page. Use this as a workaround (listening to hashchange won’t help, as we cannot access the fragment directive).
  77. addLinkClickListeners();
  78. return;
  79. }
  80. pretendBrowserSupportsTextFragments();
  81. // Run when the page is ready.
  82. window.addEventListener('load', run);
  83. // Could we somehow avoid activating in cases where the browser would retain scroll position, e.g. on page reload or history navigation?
  84. // Run whenever the location’s fragment identifier is changed.
  85. window.addEventListener('hashchange', run);
  86. // 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)
  87. }
  88. install();
  89. // A small tool to use from e.g. the browser console.
  90. export function applyFragDir(fragmentDirective) {
  91. if (typeof fragmentDirective !== 'string' || !fragmentDirective.includes(':~:'))
  92. throw new TypeError('Expected a fragment directive string, e.g. ":~:text=bla&text=blub"');
  93. fragmentDirective = fragmentDirective.substring(fragmentDirective.indexOf(':~:') + 3);
  94. applyFragmentDirective({
  95. document,
  96. documentFragmentDirective: fragmentDirective,
  97. });
  98. }
  99. //# sourceMappingURL=polyfill.js.map