contentscript.js 3.7 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import html from 'nanohtml';
  2. import { remoteFunction } from 'webextension-rpc';
  3. import { createMediaFragmentIdentifier } from '../util/media-fragment-identifier';
  4. function createPreciseUrl(url, selector) {
  5. const properUrl = url.split('#')[0];
  6. let fragmentIdentifier;
  7. if (selector.type === 'FragmentSelector') {
  8. fragmentIdentifier = selector.value;
  9. } else {
  10. throw new Error('Unsupported selector type');
  11. }
  12. return properUrl + '#' + fragmentIdentifier;
  13. }
  14. function describeMediaFragment({ start, end }) {
  15. const fragmentIdentifier = createMediaFragmentIdentifier({ start, end });
  16. const selector = {
  17. type: 'FragmentSelector',
  18. conformsTo: 'http://www.w3.org/TR/media-frags/',
  19. value: fragmentIdentifier,
  20. };
  21. return selector;
  22. }
  23. export default async function init(playlist) {
  24. let menuEl, menuItemElements;
  25. function hideMenu() {
  26. if (!menuEl) return;
  27. menuEl.parentNode.removeChild(menuEl);
  28. menuEl = undefined;
  29. document.body.removeEventListener('keydown', handleKeyDown);
  30. }
  31. function onContextMenu(event) {
  32. event.preventDefault();
  33. const left = event.pageX;
  34. const top = event.pageY;
  35. const trackDuration = playlist.duration;
  36. let { start, end } = playlist.getTimeSelection();
  37. if (start === undefined) return;
  38. // Waveform Playlist tells us end=start when the selection is just a single line.
  39. if (end === start) end = undefined;
  40. // Round the numbers to two decimals.
  41. // Also, as Waveform Playlist might report a slightly negative number, first cap it to zero.
  42. start = Math.round(Math.max(0, start) * 100) / 100;
  43. if (end !== undefined) end = Math.round(Math.max(0, end) * 100) / 100;
  44. function preciseUrl() {
  45. const fileUrl = document.URL;
  46. const selector = describeMediaFragment({ start, end });
  47. const preciseUrl = createPreciseUrl(fileUrl, selector);
  48. return preciseUrl;
  49. }
  50. const menuItems = [
  51. {
  52. title: browser.i18n.getMessage(end !== undefined
  53. ? 'copySelectionContextMenuItemForFragment'
  54. : 'copySelectionContextMenuItemForSingleMoment'
  55. ),
  56. async action() {
  57. await navigator.clipboard.writeText(preciseUrl());
  58. },
  59. },
  60. {
  61. title: browser.i18n.getMessage(end !== undefined
  62. ? 'bookmarkSelectionContextMenuItemForFragment'
  63. : 'bookmarkSelectionContextMenuItemForSingleMoment'
  64. ),
  65. async action() {
  66. await remoteFunction('createBookmark')({ url: preciseUrl(), start, end, trackDuration });
  67. },
  68. },
  69. ];
  70. menuItemElements = menuItems.map(({ title, action }) => html`
  71. <li>
  72. <button onclick=${event => { hideMenu(); action(event); }}>
  73. ${title}
  74. </button>
  75. </li>
  76. `);
  77. hideMenu();
  78. menuEl = html`
  79. <ul class="context-menu" style="top: ${top}; left: ${left};">
  80. ${menuItemElements}
  81. </ul>
  82. `;
  83. document.body.appendChild(menuEl);
  84. document.body.addEventListener('keydown', handleKeyDown);
  85. }
  86. function handleKeyDown(event) {
  87. if (event.code === 'Escape') {
  88. hideMenu();
  89. return;
  90. }
  91. const currentIndex = menuItemElements.findIndex(el => el.querySelector('button:focus'));
  92. if (event.code === 'ArrowDown') {
  93. const newIndex = (currentIndex === -1 || currentIndex === menuItemElements.length - 1)
  94. ? 0
  95. : currentIndex + 1;
  96. menuItemElements[newIndex].querySelector('button').focus();
  97. }
  98. if (event.code === 'ArrowUp') {
  99. const newIndex = (currentIndex === -1 || currentIndex === 0)
  100. ? menuItemElements.length - 1
  101. : currentIndex - 1;
  102. menuItemElements[newIndex].querySelector('button').focus();
  103. }
  104. }
  105. function onMouseDown(event) {
  106. // A mouse-down outside the menu hides the menu.
  107. if (menuEl && !menuEl.contains(event.target)) { // note nodeX.contains(nodeX) === true
  108. hideMenu();
  109. }
  110. }
  111. document.addEventListener('contextmenu', onContextMenu);
  112. document.addEventListener('mousedown', onMouseDown);
  113. }