diff --git a/app/utils/clipboard.js b/app/utils/clipboard.js index 1c3ef97..7b06678 100644 --- a/app/utils/clipboard.js +++ b/app/utils/clipboard.js @@ -7,6 +7,15 @@ * @param { String } str A string to copy * @returns { Promise. } */ -export async function copyText (str) { +export function copyText (str) { return navigator.clipboard.writeText(str) } + +/** + * Read the string stored in the clipboard, + * will return an empty string if the clipboard is empty + * @returns { Promise. } + */ +export function readText () { + return navigator.clipboard.readText() +} diff --git a/app/utils/shortcuts.js b/app/utils/shortcuts.js index 253a6cd..4ac3bd1 100644 --- a/app/utils/shortcuts.js +++ b/app/utils/shortcuts.js @@ -39,6 +39,10 @@ const timeoutOnIdle = (function () { * * - Lower case keys are all transformed to upper case * + * @example + * 'a' -> 'A' + * 'Meta' -> 'Meta' + * * @param { String } key The key to normalize * @returns { String } */ @@ -107,5 +111,5 @@ export async function registerKeyDown (e) { * @param { KeyboardEvent } e */ export function registerKeyUp (e) { - keys.delete(normalize(e.key)) + keys.clear() } diff --git a/plugins/rundown/app/components/RundownListItem/index.jsx b/plugins/rundown/app/components/RundownListItem/index.jsx index a4236ab..930f66b 100644 --- a/plugins/rundown/app/components/RundownListItem/index.jsx +++ b/plugins/rundown/app/components/RundownListItem/index.jsx @@ -110,6 +110,11 @@ export function RundownListItem ({ setIndicateIsPlaying(true) }, [item?.state, item?.didStartPlayingAt]) + async function handlePaste () { + const items = await clipboard.readJson() + bridge.commands.executeCommand('rundown.pasteItems', items, rundownId, index + 1) + } + return (
handleCopyId()} /> } + handlePaste()} /> handleAdd(newItemId)} /> diff --git a/plugins/rundown/app/utils/clipboard.js b/plugins/rundown/app/utils/clipboard.js index 1c3ef97..f730833 100644 --- a/plugins/rundown/app/utils/clipboard.js +++ b/plugins/rundown/app/utils/clipboard.js @@ -7,6 +7,29 @@ * @param { String } str A string to copy * @returns { Promise. } */ -export async function copyText (str) { +export function copyText (str) { return navigator.clipboard.writeText(str) } + +/** + * Read the string stored in the clipboard, + * will return an empty string if the clipboard is empty + * @returns { Promise. } + */ +export function readText () { + return navigator.clipboard.readText() +} + +/** + * Read the contents of the clipboard as a json object, + * will return undefined if unable to parse the data + * @returns { Promise. } + */ +export async function readJson () { + try { + const str = await readText() + return JSON.parse(str) + } catch (_) { + return undefined + } +} diff --git a/plugins/rundown/app/views/Rundown.jsx b/plugins/rundown/app/views/Rundown.jsx index 06f1f01..891ddc3 100644 --- a/plugins/rundown/app/views/Rundown.jsx +++ b/plugins/rundown/app/views/Rundown.jsx @@ -6,8 +6,10 @@ import { ContextAddMenu } from '../components/ContextAddMenu' import { ContextMenu } from '../../../../app/components/ContextMenu' import { ContextMenuItem } from '../../../../app/components/ContextMenuItem' +import { ContextMenuDivider } from '../../../../app/components/ContextMenuDivider' import * as config from '../config' +import * as clipboard from '../utils/clipboard' export function Rundown () { const [contextPos, setContextPos] = React.useState() @@ -23,12 +25,49 @@ export function Rundown () { bridge.commands.executeCommand('rundown.appendItem', rundownId, itemId) } + async function handlePaste () { + const items = await clipboard.readJson() + const selection = await bridge.client.getSelection() + + /* + Paste the items into the + selected item's parent + */ + if (selection.length) { + const item = await bridge.items.getItem(selection[0]) + bridge.commands.executeCommand('rundown.pasteItems', items, item.parent) + + /* + Paste the items into the current + rundown if no item is selected + */ + } else { + bridge.commands.executeCommand('rundown.pasteItems', items, rundownId) + } + } + + React.useEffect(() => { + function onShortcut (e) { + switch (e.detail.id) { + case 'paste': + handlePaste() + break + } + } + window.addEventListener('shortcut', onShortcut) + return () => { + window.removeEventListener('shortcut', onShortcut) + } + }, [rundownId]) + return (
handleContextMenu(e)}> { contextPos ? ( setContextPos(undefined)}> + handlePaste()} /> + handleItemCreate(newItemId)}/> diff --git a/plugins/rundown/index.js b/plugins/rundown/index.js index b5e7d04..ef699f8 100644 --- a/plugins/rundown/index.js +++ b/plugins/rundown/index.js @@ -243,6 +243,70 @@ exports.activate = async () => { } bridge.commands.registerCommand('rundown.copyItems', copyItems) + /** + * Paste items into the + * specified parent at index + * + * Will create new item id's to avoid collisions with + * already existing items but keep the item structure + * + * @param { Any[] } items An array of item objects from the clipboard + * @param { String } parentId The id of the parent to paste into + * @param { Number | undefined } index The index at which to paste the first item + * @returns { Promise. } + */ + async function pasteItems (items, parentId, index) { + if (!items || !Array.isArray(items)) { + return + } + + const idMappings = {} + + const promises = items.map(async item => { + const newId = await bridge.items.createItem(item.type) + idMappings[item.id] = newId + }) + + await Promise.all(promises) + + let i = -1 + for (const item of items) { + i++ + if (item.children && Array.isArray(item.children)) { + item.children = item.children.map(child => idMappings[child]) + } + + item.id = idMappings[item.id] + await bridge.items.applyItem(item.id, { + ...item, + parent: undefined + }) + + /* + If this is a nested item, append it into its + new parent that was just created + */ + if (item.parent && idMappings[item.parent]) { + await appendItem(idMappings[item.parent], item.id) + return + } + + /* + If this is a top level item, + paste it into the specified parent and + if specified move it to the correct index + */ + if (item.parent && !idMappings[item.parent]) { + await appendItem(parentId, item.id) + + if (index != null) { + moveItem(parentId, index + i, item.id) + } + } + } + } + bridge.commands.registerCommand('rundown.pasteItems', pasteItems) + /** * Append an item to * a new parent, will remove any current relations