From 538349e3f81731d256739a53fcfd1754f2d782e9 Mon Sep 17 00:00:00 2001 From: Soumik Rakshit Date: Wed, 26 Jun 2019 18:16:08 +0530 Subject: [PATCH 1/2] wip: add start and end time markers and create a time_block todo: Proper rendering of time_blocks --- src/EditorComponent.tsx | 21 +- src/EditorPage.jsx | 294 +++++++++++++++++++ src/keycodeMap.ts | 1 + src/prosemirror-plugins/Schema.ts | 19 +- src/prosemirror-plugins/TimeBlockNodeView.ts | 81 +++++ src/prosemirror-plugins/commands.ts | 117 +++++++- src/prosemirror-plugins/inputrules.js | 25 ++ src/prosemirror-plugins/timemarker.js | 9 + 8 files changed, 557 insertions(+), 10 deletions(-) create mode 100755 src/EditorPage.jsx create mode 100644 src/prosemirror-plugins/TimeBlockNodeView.ts create mode 100644 src/prosemirror-plugins/inputrules.js create mode 100644 src/prosemirror-plugins/timemarker.js diff --git a/src/EditorComponent.tsx b/src/EditorComponent.tsx index bfb8a63..9ffb518 100644 --- a/src/EditorComponent.tsx +++ b/src/EditorComponent.tsx @@ -12,6 +12,7 @@ import { makeStyles, createStyles } from '@material-ui/core/styles'; import AppConfig from './AppConfig'; import EditorSchema from './prosemirror-plugins/Schema'; import ImageNodeView from './prosemirror-plugins/ImageNodeView'; +import TimeBlockNodeView from './prosemirror-plugins/TimeBlockNodeView'; import AutosavePlugin from './prosemirror-plugins/AutosavePlugin'; import { loadNote } from './LocalStorageHelper'; import keycodes from './keycodeMap'; @@ -21,7 +22,13 @@ import { mkSeekToTimestamp, mkToggleTimestampMark, textToTimestamp, + mkWrapInTimeBlock, } from './prosemirror-plugins/commands'; +import { + mkToggleVideoPauseInputRule, + ItalicStartTimeInputRule, + ItalicEndTimeInputRule, +} from './prosemirror-plugins/inputrules'; import 'prosemirror-view/style/prosemirror.css'; import 'prosemirror-menu/style/menu.css'; @@ -44,7 +51,7 @@ const EditorComponent = (props: any) => { const editorView = useRef(); const classes = useStyles(); - const { player: playerRef, videoId, videoTitle } = props; + const { player: playerRef, videoId, videoTitle, videoDuration } = props; const player = playerRef.current; @@ -92,8 +99,8 @@ const EditorComponent = (props: any) => { put_timestamp: mkInsertTimestampStr(currentTimestamp), seek_to_timestamp: mkSeekToTimestamp(videoTime => player.seek(videoTime)), capture_frame: onCaptureFrame, - debug_print: () => true, turn_text_to_timestamp: textToTimestamp, + wrap_in_time_block: mkWrapInTimeBlock(videoDuration), }; const keymapObject = Object.keys(keycodes) @@ -124,7 +131,11 @@ const EditorComponent = (props: any) => { autosavePlugin, inputRules({ - rules: [ToggleVideoPauseInputRule], + rules: [ + // mkToggleVideoPauseInputRule(doCommand), + ItalicStartTimeInputRule, + ItalicEndTimeInputRule, + ], }), ...exampleSetup({ schema: EditorSchema }), ], @@ -134,6 +145,10 @@ const EditorComponent = (props: any) => { inlineImage: (node, view, getPos) => { return new ImageNodeView(node, view, getPos); }, + + time_block: (node, view, getPos) => { + return new TimeBlockNodeView(node, view, getPos); + }, }, }); diff --git a/src/EditorPage.jsx b/src/EditorPage.jsx new file mode 100755 index 0000000..c117d2d --- /dev/null +++ b/src/EditorPage.jsx @@ -0,0 +1,294 @@ +import React, { Component } from 'react'; +import { withRouter } from 'react-router-dom'; + +import EditorComponent from './EditorComponent'; +import VideoPathInput from './VideoPathInput'; +import getYoutubeTitle from 'get-youtube-title'; +import PropTypes from 'prop-types'; +import IFrameStyleWrapper from './IFrameStyleWrapper'; +import { SnackbarContext } from './context/SnackbarContext'; +import AppConfig from './AppConfig'; + +const YOUTUBE_API_KEY = process.env.REACT_APP_YOUTUBE_API_KEY; +if (!YOUTUBE_API_KEY) { + alert('REACT_APP_YOUTUBE_API_KEY required in .env file'); +} + +const ytNameOfPlayerState = { + '-1': 'unstarted', + '0': 'ended', + '1': 'playing', + '2': 'paused', + '3': 'buffering', + '5': 'video_cued', +}; + +class YoutubePlayerController { + constructor(YT, playerApi) { + if (!playerApi) { + throw new Error('playerApi is null'); + } + this.YT = YT; + this.playerApi = playerApi; + this.currentVideoId = null; + this.currentVideoTitle = null; + this.currentPlayerState = 'unstarted'; + } + + setVideoTitle() { + if (this.currentVideoId === null) { + return; + } + + this.currentVideoTitle = null; + + getYoutubeTitle(this.currentVideoId, YOUTUBE_API_KEY, (err, title) => { + if (!err) { + this.currentVideoTitle = title; + } else { + return new Error(`Failed to retrive title of video - ${this.currentVideoId}`); + } + }); + } + + getPlayerState() { + return this.playerApi.getPlayerState(); + } + + playVideo(videoId = null) { + if (!videoId && !this.currentVideoId) { + return; + } + + this.currentVideoId = videoId ? videoId : this.currentVideoId; + + this.playerApi.playVideo(this.currentVideoId); + } + + loadAndPlayVideo(videoId) { + this.currentVideoId = videoId; + this.currentVideoTitle = null; + this.playerApi.cueVideoById(this.currentVideoId, 0); + this.playerApi.playVideo(this.currentVideoId); + this.setVideoTitle(); + } + + pauseVideo() { + this.playerApi.pauseVideo(this.currentVideoId); + } + + // Returns state after toggle + togglePause() { + if (this.currentPlayerState === 'paused') { + this.playVideo(); + return 'playing'; + } else if (this.currentPlayerState === 'playing') { + this.pauseVideo(); + return 'paused'; + } + return null; + } + + addToCurrentTime(seconds) { + const currentTime = this.playerApi.getCurrentTime(); + this.playerApi.seekTo(Math.max(currentTime + seconds, 0)); + } + + getCurrentTime() { + return this.playerApi && this.playerApi.getCurrentTime ? this.playerApi.getCurrentTime() : null; + } + + getVideoTitle() { + return this.currentVideoTitle; + } + + getDuration() { + return this.playerApi && this.playerApi.getDuration ? this.playerApi.getDuration() : null; + } + + seekTo(timeInSeconds) { + this.playerApi.seekTo(timeInSeconds); + } +} + +// The commands from console are send via the App component +class EditorPage extends Component { + static propTypes = { + startingVideoId: PropTypes.string, + startingPopperMessage: PropTypes.string, + }; + + static defaultProps = { + startingVideoId: null, + startingPopperMessage: null, + }; + + static contextType = SnackbarContext; + + constructor(props) { + super(props); + + this.state = { + infoText: null, + infoLastTime: null, + selectedOption: null, + startingPopperMessage: this.props.startingPopperMessage, + videoId: null, + }; + + // We keep a handle to the youtube player. This is the player API object, not the dom + // element itself. + this.ytPlayerController = null; + this.doVideoCommand = this.doVideoCommand.bind(this); + + this.iframeRef = React.createRef(); + } + + tellPluginToRemovePauseOverlay = () => { + window.frames[0].postMessage({ type: AppConfig.RemovePauseOverlayMessage }, '*'); + }; + + doVideoCommand = (command, params) => { + const currentTime = this.ytPlayerController.getCurrentTime(); + + switch (command) { + case 'playVideo': + this.ytPlayerController.playVideo(); + break; + + case 'pauseVideo': + this.ytPlayerController.pauseVideo(); + this.tellPluginToRemovePauseOverlay(); + break; + + case 'restartVideo': + this.ytPlayerController.seekTo(0); + break; + + case 'togglePause': + const playState = this.ytPlayerController.togglePause(); + if (playState === 'paused') { + this.tellPluginToRemovePauseOverlay(); + } + break; + + case 'addToCurrentTime': + this.ytPlayerController.addToCurrentTime(params.secondsToAdd); + break; + + case 'seekToTime': + if (params.videoTime) { + this.ytPlayerController.seekTo(params.videoTime); + } + break; + + case 'currentTime': + break; + + default: + break; + } + + return currentTime; + }; + + currentVideoInfo = () => { + const info = { videoId: null, videoTime: null, videoTitle: null }; + if (this.ytPlayerController) { + info.videoId = this.ytPlayerController.currentVideoId; + info.videoTime = this.ytPlayerController.getCurrentTime(); + info.videoTitle = this.ytPlayerController.getVideoTitle(); + info.videoDuration = this.ytPlayerController.getDuration(); + } + return info; + }; + + componentDidMount() { + const { ytAPI } = this.props; + if (this.iframeRef.current) { + let ytPlayerApi = null; + + const { startingVideoId } = this.props; + + ytPlayerApi = new ytAPI.Player(this.iframeRef.current, { + height: '100%', + width: '100%', + events: { + onStateChange: newState => { + this.ytPlayerController.currentPlayerState = ytNameOfPlayerState[newState.data]; + }, + + onReady: () => { + this.ytPlayerController = new YoutubePlayerController(ytAPI, ytPlayerApi); + if (startingVideoId) { + this.ytPlayerController.loadAndPlayVideo(startingVideoId); + // console.log('Loading video and note for ', this.props.startingVideoId); + const videoId = this.props.startingVideoId; + + this.context.openSnackbar({ + message: `Loading video: ${videoId}`, + autoHideDuration: 1000, + }); + + this.setState({ videoId }); + } + }, + }, + }); + } + + if (this.state.startingPopperMessage) { + this.context.openSnackbar({ message: this.state.startingPopperMessage }); + setTimeout(() => { + this.setState({ startingPopperMessage: null }); + }); + } + } + + render() { + const { match } = this.props; + const videoId = match.params.videoId; + + if (!videoId) { + return ; + } + + const info = this.currentVideoInfo(); + + console.log('videoDuration =', videoDuration); + + return ( +
+ +
+ + +
+ ); + } +} + +export default withRouter(EditorPage); diff --git a/src/keycodeMap.ts b/src/keycodeMap.ts index dc2ad17..cffe9cf 100644 --- a/src/keycodeMap.ts +++ b/src/keycodeMap.ts @@ -6,6 +6,7 @@ const keycodes: { [s: string]: string } = { seek_to_timestamp: 'Ctrl-g', capture_frame: 'Ctrl-i', debug_print: 'Ctrl-d', + wrap_in_time_block: 'Ctrl-o', }; export default keycodes; diff --git a/src/prosemirror-plugins/Schema.ts b/src/prosemirror-plugins/Schema.ts index 136e809..aa3e8bb 100644 --- a/src/prosemirror-plugins/Schema.ts +++ b/src/prosemirror-plugins/Schema.ts @@ -2,6 +2,7 @@ import { Schema, NodeSpec, MarkSpec } from 'prosemirror-model'; type NodeSpecKeys = | 'doc' + | 'time_block' | 'paragraph' | 'blockquote' | 'horizontal_rule' @@ -16,7 +17,17 @@ type MarkKeys = 'link' | 'em' | 'strong' | 'code' | 'timestamp'; const NodeSpecs: { [name in NodeSpecKeys]: NodeSpec } = { doc: { + content: '(block | time_block)+', + }, + + time_block: { + attrs: { + startTime: {}, + endTime: {}, + videoDuration: {}, + }, content: 'block+', + parseDOM: [{ tag: 'time-block' }], }, paragraph: { @@ -53,7 +64,8 @@ const NodeSpecs: { [name in NodeSpecKeys]: NodeSpec } = { { tag: 'h5', attrs: { level: 5 } }, { tag: 'h6', attrs: { level: 6 } }, ], - toDOM(node) { + + toDOM: node => { return ['h' + node.attrs.level, 0]; }, }, @@ -216,7 +228,6 @@ const EditorSchema = new Schema({ nodes: NodeSpecs, mark export default EditorSchema; const ImageNodeType = EditorSchema.nodes['inlineImage']; +const TimeBlockNodeType = EditorSchema.nodes['time_block']; -const WithTimeRangeNodeType = EditorSchema.nodes['withTimeRange']; - -export { ImageNodeType, WithTimeRangeNodeType }; +export { ImageNodeType }; diff --git a/src/prosemirror-plugins/TimeBlockNodeView.ts b/src/prosemirror-plugins/TimeBlockNodeView.ts new file mode 100644 index 0000000..2348de7 --- /dev/null +++ b/src/prosemirror-plugins/TimeBlockNodeView.ts @@ -0,0 +1,81 @@ +import { Node } from 'prosemirror-model'; +import { EditorView, NodeView, Decoration } from 'prosemirror-view'; +import EditorSchema from './Schema'; + +const fract = (n: number) => n - Math.floor(n); + +const hash = (f: number) => fract(Math.sin(f) * 10000); + +function clamp255(n: number) { + return Math.min(Math.max(0, n), 255); +} + +function toHex(value: number) { + value = clamp255(value); + value = Math.floor(value) % 255; + let hex = value.toString(16); + if (hex.length != 2) { + hex = '0' + hex; + } + return hex; +} + +function intervalHtmlColor(startTime: number, endTime: number, fullLength: number) { + let rn = (endTime - startTime) / fullLength; + let gn = hash(startTime / fullLength); + let bn = hash((endTime * startTime) & 0xffea90ff); + + rn = rn * 255; + gn = gn * 255; + bn = bn * 255; + + const r = toHex(rn); + const g = toHex(gn); + const b = toHex(bn); + return `#${r}${g}${b}ff`; +} + +class TimeBlockNodeView implements NodeView { + _node: Node; + _view: EditorView; + _getPos: () => number; + _typename: string; + + dom: HTMLElement; + contentDOM: HTMLElement; + + constructor(node: Node, view: EditorView, getPos: () => number) { + this._node = node; + this._view = view; + this._getPos = getPos; + + this._typename = node.type.name; + + this.dom = document.createElement('div'); + this.contentDOM = this.dom; + + this.dom.classList.add('time-block'); + + this.update(node, []); + } + + update(node: Node, decorations: Decoration[]) { + if (node.type.name !== this._typename) { + return false; + } + + this.dom.style.backgroundColor = intervalHtmlColor( + node.attrs.startTime, + node.attrs.endTime, + node.attrs.videoDuration + ); + + return true; + } + + destroy() { + this.dom.remove(); + } +} + +export default TimeBlockNodeView; diff --git a/src/prosemirror-plugins/commands.ts b/src/prosemirror-plugins/commands.ts index 2db89fb..62c4118 100644 --- a/src/prosemirror-plugins/commands.ts +++ b/src/prosemirror-plugins/commands.ts @@ -1,10 +1,12 @@ import secondsToHhmmss from '../utils/secondsToHhmmsss'; import EditorSchema, { ImageNodeType } from './Schema'; +import { startTimeMark, endTimeMark } from './timemarker'; -import { toggleMark } from 'prosemirror-commands'; -import { findTextNodes } from 'prosemirror-utils'; -import { EditorState } from 'prosemirror-state'; +import { toggleMark, wrapIn } from 'prosemirror-commands'; +import { findTextNodes, findParentNode } from 'prosemirror-utils'; +import { EditorState, Selection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; +import { Node } from 'prosemirror-model'; const floorOrZero = (n: number) => (Number.isNaN(n) ? 0 : Math.floor(n)); @@ -132,3 +134,112 @@ export const mkSeekToTimestamp = (seekTo: (ts: number) => void) => (state: Edito return true; }; + +const getTextFromSelection = (state: EditorState) => { + const { selection } = state; + + if (selection.empty) { + return ''; + } + + const fragment = state.doc.cut(selection.from, selection.to); + const textNodes = findTextNodes(fragment); + + const text = textNodes.reduce((acc, { node }) => acc + node.text, ''); + + return text; +}; + +function findTimeRange(selection: Selection) { + const { $from, $to } = selection; + + if (!$from.parent.eq($to.parent)) { + console.log('findTimeRange - not same parent'); + } +} + +// Regex can match ':20' instead of just '20'. +const parseTimeUnit = (s: string) => parseFloat(s[0] === ':' ? s.substring(1, s.length) : s); + +function secondsFromTimeMark(match: any) { + let secs = 0; + console.log('match =', match); + if (match[4] !== undefined) { + console.log('match[4] !== undefined'); + secs += parseTimeUnit(match[4]); + secs += parseTimeUnit(match[3]) * 60; + secs += parseTimeUnit(match[1]) * 3600; + console.log('secs =', secs); + } else if (match[3] !== undefined) { + secs += parseTimeUnit(match[3]); + secs += parseTimeUnit(match[1]) * 60; + } else { + if (match[1] === undefined) { + console.warn('Unexpected time mark -', match[0]); + return 0; + } + secs = parseTimeUnit(match[1]); + } + return secs; +} + +export const mkWrapInTimeBlock = (videoDuration: number) => (state: EditorState, dispatch: any) => { + console.log('WrapInTimeBlock'); + + const { selection } = state; + + const predicate = (node: Node) => node.type !== EditorSchema.nodes.time_block; + const parent = findParentNode(predicate)(selection); + + console.log('parent =', parent); + console.log('Block range'); + + const { $from, $to } = selection; + + const nodeRange = $from.blockRange($to); + + if (!nodeRange) { + return true; + } + + const { $from: rangeFrom, $to: rangeTo } = nodeRange; + + console.log('nodeRange.from node =', rangeFrom.node()); + console.log('nodeRange.to node =', rangeTo.node()); + console.log('Text in selection =', getTextFromSelection(state)); + + // Search for time start and end marker + { + const text = getTextFromSelection(state); + + const startTimeMatch = text.match(startTimeMark); + if (!startTimeMatch) { + return true; + } + + const endTimeMatch = text.match(endTimeMark); + if (!endTimeMatch) { + return true; + } + + // Check they do form a proper block + + if (startTimeMatch.index! >= endTimeMatch.index!) { + return true; + } + + const startTime = secondsFromTimeMark(startTimeMatch); + const endTime = secondsFromTimeMark(endTimeMatch); + + console.log('startTime =', startTime, 'endTime =', endTime, 'videoDuration =', videoDuration); + + // Wrap in time_block + return wrapIn(EditorSchema.nodes.time_block, { + startTime, + endTime, + videoDuration, + })(state, dispatch); + } + + return true; +}; diff --git a/src/prosemirror-plugins/inputrules.js b/src/prosemirror-plugins/inputrules.js new file mode 100644 index 0000000..759144d --- /dev/null +++ b/src/prosemirror-plugins/inputrules.js @@ -0,0 +1,25 @@ +import { InputRule } from 'prosemirror-inputrules'; + +import EditorSchema from './Schema'; +import { startTimeMarkIR, endTimeMarkIR } from './timemarker'; + +// Pause input rule. Typing "#." will toggle pause. +export const mkToggleVideoPauseInputRule = doCommand => { + return new InputRule(/#\.$/, (state, match, start, end) => { + doCommand('togglePause'); + return state.tr.insertText('', start, end); + }); +}; + +function makeItalic(state, match, start, end) { + const mark = EditorSchema.marks.em.create({}); + const text = match[0]; + return state.tr + .addStoredMark(mark) + .insertText(text, start, end) + .removeStoredMark(mark); +} + +export const ItalicStartTimeInputRule = new InputRule(startTimeMarkIR, makeItalic); + +export const ItalicEndTimeInputRule = new InputRule(endTimeMarkIR, makeItalic); diff --git a/src/prosemirror-plugins/timemarker.js b/src/prosemirror-plugins/timemarker.js new file mode 100644 index 0000000..2b6bfea --- /dev/null +++ b/src/prosemirror-plugins/timemarker.js @@ -0,0 +1,9 @@ +// Allowed time markers are of the form +// @start(09)@ --> Denotes 9 seconds into the video +// @start(09:10)@ --> Denotes 9 minutes, 10 seconds into the video +// @start(01:09:20)@ --> Denotes 1 hour 9 minutes 20 seconds into the video. +export const startTimeMark = /@start\(([0-9]{1,2})((:[0-9]{1,2})(:[0-9]{1,2})?)?\)@/; +export const endTimeMark = /@end\(([0-9]{1,2})((:[0-9]{1,2})(:[0-9]{1,2})?)?\)@/; + +export const startTimeMarkIR = /@start\(([0-9]{1,2})((:[0-9]{1,2})(:[0-9]{1,2})?)?\)@$/; +export const endTimeMarkIR = /@end\(([0-9]{1,2})((:[0-9]{1,2})(:[0-9]{1,2})?)?\)@$/; From cf1093ea239cfce0554cafd528be02e6e306ff86 Mon Sep 17 00:00:00 2001 From: Soumik Rakshit Date: Wed, 10 Jul 2019 12:53:41 +0530 Subject: [PATCH 2/2] saving --- src/EditorComponent.tsx | 5 ++++- src/keycodeMap.ts | 1 + src/prosemirror-plugins/TimeBlockNodeView.ts | 12 +++++------ src/prosemirror-plugins/commands.ts | 21 +++++++++++++++++++- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/EditorComponent.tsx b/src/EditorComponent.tsx index 9ffb518..5bf64e8 100644 --- a/src/EditorComponent.tsx +++ b/src/EditorComponent.tsx @@ -4,7 +4,7 @@ import { Node } from 'prosemirror-model'; import { EditorView } from 'prosemirror-view'; import { exampleSetup } from 'prosemirror-example-setup'; import { keymap } from 'prosemirror-keymap'; -import { InputRule, inputRules } from 'prosemirror-inputrules'; +import { inputRules } from 'prosemirror-inputrules'; import { DOMParser } from 'prosemirror-model'; import Paper from '@material-ui/core/Paper'; import { makeStyles, createStyles } from '@material-ui/core/styles'; @@ -23,12 +23,14 @@ import { mkToggleTimestampMark, textToTimestamp, mkWrapInTimeBlock, + unwrapAllTimeBlocks, } from './prosemirror-plugins/commands'; import { mkToggleVideoPauseInputRule, ItalicStartTimeInputRule, ItalicEndTimeInputRule, } from './prosemirror-plugins/inputrules'; +import { InputRule } from 'prosemirror-inputrules'; import 'prosemirror-view/style/prosemirror.css'; import 'prosemirror-menu/style/menu.css'; @@ -101,6 +103,7 @@ const EditorComponent = (props: any) => { capture_frame: onCaptureFrame, turn_text_to_timestamp: textToTimestamp, wrap_in_time_block: mkWrapInTimeBlock(videoDuration), + unwrap_all_time_blocks: unwrapAllTimeBlocks, }; const keymapObject = Object.keys(keycodes) diff --git a/src/keycodeMap.ts b/src/keycodeMap.ts index cffe9cf..d340780 100644 --- a/src/keycodeMap.ts +++ b/src/keycodeMap.ts @@ -7,6 +7,7 @@ const keycodes: { [s: string]: string } = { capture_frame: 'Ctrl-i', debug_print: 'Ctrl-d', wrap_in_time_block: 'Ctrl-o', + unwrap_time_blocks: 'Ctrl-p', }; export default keycodes; diff --git a/src/prosemirror-plugins/TimeBlockNodeView.ts b/src/prosemirror-plugins/TimeBlockNodeView.ts index 2348de7..accb371 100644 --- a/src/prosemirror-plugins/TimeBlockNodeView.ts +++ b/src/prosemirror-plugins/TimeBlockNodeView.ts @@ -14,7 +14,7 @@ function toHex(value: number) { value = clamp255(value); value = Math.floor(value) % 255; let hex = value.toString(16); - if (hex.length != 2) { + if (hex.length !== 2) { hex = '0' + hex; } return hex; @@ -64,11 +64,11 @@ class TimeBlockNodeView implements NodeView { return false; } - this.dom.style.backgroundColor = intervalHtmlColor( - node.attrs.startTime, - node.attrs.endTime, - node.attrs.videoDuration - ); + const color = intervalHtmlColor(node.attrs.startTime, node.attrs.endTime, node.attrs.duration); + console.log('color =', color); + + this.dom.style.backgroundColor = color; + this.dom.style.marginLeft = '10px'; return true; } diff --git a/src/prosemirror-plugins/commands.ts b/src/prosemirror-plugins/commands.ts index 62c4118..e8cd34f 100644 --- a/src/prosemirror-plugins/commands.ts +++ b/src/prosemirror-plugins/commands.ts @@ -3,7 +3,7 @@ import EditorSchema, { ImageNodeType } from './Schema'; import { startTimeMark, endTimeMark } from './timemarker'; import { toggleMark, wrapIn } from 'prosemirror-commands'; -import { findTextNodes, findParentNode } from 'prosemirror-utils'; +import { findTextNodes, findParentNode, findChildren } from 'prosemirror-utils'; import { EditorState, Selection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { Node } from 'prosemirror-model'; @@ -183,6 +183,25 @@ function secondsFromTimeMark(match: any) { return secs; } +// A command that unwraps all time_block nodes in current selection +export const unwrapAllTimeBlocks = (state: EditorState, dispatch: any) => { + console.log('UnwrapAllTimeBlocks'); + + const { selection } = state; + const { $from, $to } = selection; + + const nodeRange = $from.blockRange($to); + + const containerNode = nodeRange!.$from.node(); + + const startOffset = nodeRange!.$from.parentOffset; + const startChild = containerNode!.child(startOffset); + + console.log('startChild =', startChild); + + return true; +}; + export const mkWrapInTimeBlock = (videoDuration: number) => (state: EditorState, dispatch: any) => { console.log('WrapInTimeBlock');