import html from 'nanohtml'; import { remoteFunction } from 'webextension-rpc'; import { createMediaFragmentIdentifier } from '../util/media-fragment-identifier'; function createPreciseUrl(url, selector) { const properUrl = url.split('#')[0]; let fragmentIdentifier; if (selector.type === 'FragmentSelector') { fragmentIdentifier = selector.value; } else { throw new Error('Unsupported selector type'); } return properUrl + '#' + fragmentIdentifier; } function describeMediaFragment({ start, end }) { const fragmentIdentifier = createMediaFragmentIdentifier({ start, end }); const selector = { type: 'FragmentSelector', conformsTo: 'http://www.w3.org/TR/media-frags/', value: fragmentIdentifier, }; return selector; } export default async function init(playlist) { let menuEl, menuItemElements; function hideMenu() { if (!menuEl) return; menuEl.parentNode.removeChild(menuEl); menuEl = undefined; document.body.removeEventListener('keydown', handleKeyDown); } function onContextMenu(event) { event.preventDefault(); const left = event.pageX; const top = event.pageY; const trackDuration = playlist.duration; let { start, end } = playlist.getTimeSelection(); if (start === undefined) return; // Waveform Playlist tells us end=start when the selection is just a single line. if (end === start) end = undefined; // Round the numbers to two decimals. // Also, as Waveform Playlist might report a slightly negative number, first cap it to zero. start = Math.round(Math.max(0, start) * 100) / 100; if (end !== undefined) end = Math.round(Math.max(0, end) * 100) / 100; function preciseUrl() { const fileUrl = document.URL; const selector = describeMediaFragment({ start, end }); const preciseUrl = createPreciseUrl(fileUrl, selector); return preciseUrl; } const menuItems = [ { title: browser.i18n.getMessage(end !== undefined ? 'copySelectionContextMenuItemForFragment' : 'copySelectionContextMenuItemForSingleMoment' ), async action() { await navigator.clipboard.writeText(preciseUrl()); }, }, { title: browser.i18n.getMessage(end !== undefined ? 'bookmarkSelectionContextMenuItemForFragment' : 'bookmarkSelectionContextMenuItemForSingleMoment' ), async action() { await remoteFunction('createBookmark')({ url: preciseUrl(), start, end, trackDuration }); }, }, ]; menuItemElements = menuItems.map(({ title, action }) => html`
  • `); hideMenu(); menuEl = html` `; document.body.appendChild(menuEl); document.body.addEventListener('keydown', handleKeyDown); } function handleKeyDown(event) { if (event.code === 'Escape') { hideMenu(); return; } const currentIndex = menuItemElements.findIndex(el => el.querySelector('button:focus')); if (event.code === 'ArrowDown') { const newIndex = (currentIndex === -1 || currentIndex === menuItemElements.length - 1) ? 0 : currentIndex + 1; menuItemElements[newIndex].querySelector('button').focus(); } if (event.code === 'ArrowUp') { const newIndex = (currentIndex === -1 || currentIndex === 0) ? menuItemElements.length - 1 : currentIndex - 1; menuItemElements[newIndex].querySelector('button').focus(); } } function onMouseDown(event) { // A mouse-down outside the menu hides the menu. if (menuEl && !menuEl.contains(event.target)) { // note nodeX.contains(nodeX) === true hideMenu(); } } document.addEventListener('contextmenu', onContextMenu); document.addEventListener('mousedown', onMouseDown); }