@bbc/react-transcript-editor
Version:
A React component to make transcribing audio and video easier and faster.
41 lines • 18.8 kB
JavaScript
;Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=void 0;var _react=_interopRequireDefault(require("react")),_propTypes=_interopRequireDefault(require("prop-types")),_reactSimpleTooltip=_interopRequireDefault(require("react-simple-tooltip")),_reactFontawesome=require("@fortawesome/react-fontawesome"),_freeSolidSvgIcons=require("@fortawesome/free-solid-svg-icons"),_draftJs=require("draft-js"),_Word=_interopRequireDefault(require("./Word")),_WrapperBlock=_interopRequireDefault(require("./WrapperBlock")),_index=_interopRequireDefault(require("../../Util/adapters/index.js")),_index2=_interopRequireDefault(require("../../Util/export-adapters/index.js")),_indexModule=_interopRequireDefault(require("./index.module.css"));function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _typeof(obj){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(obj){return typeof obj}:function(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj},_typeof(obj)}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _defineProperties(target,props){for(var descriptor,i=0;i<props.length;i++)descriptor=props[i],descriptor.enumerable=descriptor.enumerable||!1,descriptor.configurable=!0,"value"in descriptor&&(descriptor.writable=!0),Object.defineProperty(target,descriptor.key,descriptor)}function _createClass(Constructor,protoProps,staticProps){return protoProps&&_defineProperties(Constructor.prototype,protoProps),staticProps&&_defineProperties(Constructor,staticProps),Constructor}function _possibleConstructorReturn(self,call){return call&&("object"===_typeof(call)||"function"==typeof call)?call:_assertThisInitialized(self)}function _getPrototypeOf(o){return _getPrototypeOf=Object.setPrototypeOf?Object.getPrototypeOf:function(o){return o.__proto__||Object.getPrototypeOf(o)},_getPrototypeOf(o)}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function");subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,writable:!0,configurable:!0}}),superClass&&_setPrototypeOf(subClass,superClass)}function _setPrototypeOf(o,p){return _setPrototypeOf=Object.setPrototypeOf||function(o,p){return o.__proto__=p,o},_setPrototypeOf(o,p)}function _assertThisInitialized(self){if(void 0===self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return self}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}var hasCommandModifier=_draftJs.KeyBindingUtil.hasCommandModifier,TimedTextEditor=/*#__PURE__*/function(_React$Component){function TimedTextEditor(props){var _this;return _classCallCheck(this,TimedTextEditor),_this=_possibleConstructorReturn(this,_getPrototypeOf(TimedTextEditor).call(this,props)),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"onChange",function(editorState){// https://draftjs.org/docs/api-reference-editor-state#lastchangetype
// https://draftjs.org/docs/api-reference-editor-change-type
// doing editorStateChangeType === 'insert-characters' is triggered even
// outside of draftJS eg when clicking play button so using this instead
// see issue https://github.com/facebook/draft-js/issues/1060
if(_this.state.editorState.getCurrentContent()!==editorState.getCurrentContent()&&_this.props.isPauseWhileTypingOn&&_this.props.isPlaying()){_this.props.playMedia(!1);clearTimeout(_this.plauseWhileTypingTimeOut),_this.plauseWhileTypingTimeOut=setTimeout(function(){this.props.playMedia(!0)}.bind(_assertThisInitialized(_assertThisInitialized(_this))),3e3)}_this.state.isEditable&&_this.setState(function(){return{editorState:editorState}},function(){void 0!==_this.saveTimer&&clearTimeout(_this.saveTimer),_this.saveTimer=setTimeout(function(){_this.localSave(_this.props.mediaUrl)},5e3)})}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"handleDoubleClick",function(event){// nativeEvent --> React giving you the DOM event
// find the parent in Word that contains span with time-code start attribute
for(var element=event.nativeEvent.target;!element.hasAttribute("data-start")&&element.parentElement;)element=element.parentElement;if(element.hasAttribute("data-start")){var t=parseFloat(element.getAttribute("data-start"));_this.props.onWordClick(t)}}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"localSave",function(){console.log("localSave"),clearTimeout(_this.saveTimer);var mediaUrlName=_this.props.mediaUrl;// if using local media instead of using random blob name
// that makes it impossible to retrieve from on page refresh
// use file name
_this.props.mediaUrl.includes("blob")&&(mediaUrlName=_this.props.fileName);var data=(0,_draftJs.convertToRaw)(_this.state.editorState.getCurrentContent());localStorage.setItem("draftJs-".concat(mediaUrlName),JSON.stringify(data));var newLastLocalSavedDate=new Date().toString();localStorage.setItem("timestamp-".concat(mediaUrlName),newLastLocalSavedDate)}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"getWordCount",function(editorState){var plainText=editorState.getCurrentContent().getPlainText(""),regex=/(?:\r\n|\r|\n)/g,cleanString=plainText.replace(regex," ").trim(),wordArray=cleanString.match(/\S+/g);// matches words according to whitespace
return wordArray?wordArray.length:0}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"setEditorContentState",function(data){var contentState=(0,_draftJs.convertFromRaw)(data),editorState=_draftJs.EditorState.createWithContent(contentState,decorator);// eslint-disable-next-line no-use-before-define
void 0!==_this.props.handleAnalyticsEvents&&_this.props.handleAnalyticsEvents({category:"TimedTextEditor",action:"setEditorContentState",name:"getWordCount",value:_this.getWordCount(editorState)}),_this.setState({editorState:editorState})}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"forceRenderDecorator",function(){// const { editorState, updateEditorState } = this.props;
var contentState=_this.state.editorState.getCurrentContent(),decorator=_this.state.editorState.getDecorator(),newState=_draftJs.EditorState.createWithContent(contentState,decorator),newEditorState=_draftJs.EditorState.push(newState,contentState);_this.setState({editorState:newEditorState})}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"setEditorNewContentState",function(newContentState){var newEditorState=_draftJs.EditorState.push(_this.state.editorState,newContentState);_this.setState({editorState:newEditorState})}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"customKeyBindingFn",function(e){var spaceKey=32;return e.keyCode===13?"split-paragraph":e.altKey&&(e.keyCode===spaceKey||e.keyCode===spaceKey||e.keyCode===75||e.keyCode===76||e.keyCode===74||e.keyCode===187||e.keyCode===189||e.keyCode===82||e.keyCode===84)?(e.preventDefault(),"keyboard-shortcuts"):(0,_draftJs.getDefaultKeyBinding)(e);// if alt key is pressed in combination with these other keys
}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"handleKeyCommand",function(command){return"split-paragraph"===command&&_this.splitParagraph(),"keyboard-shortcuts"===command?"handled":"not-handled"}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"splitParagraph",function(){// https://github.com/facebook/draft-js/issues/723#issuecomment-367918580
// https://draftjs.org/docs/api-reference-selection-state#start-end-vs-anchor-focus
var currentSelection=_this.state.editorState.getSelection();// only perform if selection is not selecting a range of words
// in that case, we'd expect delete + enter to achieve same result.
if(currentSelection.isCollapsed()){var currentContent=_this.state.editorState.getCurrentContent(),newContentState=_draftJs.Modifier.splitBlock(currentContent,currentSelection),splitState=_draftJs.EditorState.push(_this.state.editorState,newContentState,"split-block"),targetSelection=splitState.getSelection(),originalBlock=currentContent.blockMap.get(newContentState.selectionBefore.getStartKey()),originalBlockData=originalBlock.getData(),blockSpeaker=originalBlockData.get("speaker"),wordStartTime="NA",isEndOfParagraph=!1,entityKey=originalBlock.getEntityAt(currentSelection.getStartOffset());// https://draftjs.org/docs/api-reference-modifier#splitblock
// if there is no word entity associated with a char then there is no entity key
// at that selection point
if(null===entityKey){var closestEntityToSelection=_this.findClosestEntityKeyToSelectionPoint(currentSelection,originalBlock);// handle edge case when it doesn't find a closest entity (word)
// eg pres enter on an empty line
if(entityKey=closestEntityToSelection.entityKey,isEndOfParagraph=closestEntityToSelection.isEndOfParagraph,null===entityKey)return"not-handled"}// if there is an entityKey at or close to the selection point
// can get the word startTime. for the new paragraph.
var entityInstance=currentContent.getEntity(entityKey),entityData=entityInstance.getData();wordStartTime=isEndOfParagraph?entityData.end:entityData.start;// split paragraph
// https://draftjs.org/docs/api-reference-modifier#mergeblockdata
var afterMergeContentState=_draftJs.Modifier.mergeBlockData(splitState.getCurrentContent(),targetSelection,{start:wordStartTime,speaker:blockSpeaker});return _this.setEditorNewContentState(afterMergeContentState),"handled"}return"not-handled"}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"findClosestEntityKeyToSelectionPoint",function(currentSelection,originalBlock){// set defaults
var entityKey=null,isEndOfParagraph=!1,startSelectionOffsetKey=currentSelection.getStartOffset(),lengthPlainTextForTheBlock=originalBlock.getLength(),remainingCharNumber=lengthPlainTextForTheBlock-startSelectionOffsetKey;// if it's the last char in the paragraph - get previous entity
if(0===remainingCharNumber){isEndOfParagraph=!0;for(var j=lengthPlainTextForTheBlock;0<j;j--)if(entityKey=originalBlock.getEntityAt(j),null!==entityKey)// if it finds it then return
return{entityKey:entityKey,isEndOfParagraph:isEndOfParagraph}}// if it's first char or another within the block - get next entity
else{console.log("Main part of paragraph");for(var initialSelectionOffset=currentSelection.getStartOffset(),i=0;i<remainingCharNumber;i++)// if it finds it then return
if(initialSelectionOffset+=i,entityKey=originalBlock.getEntityAt(initialSelectionOffset),null!==entityKey)return{entityKey:entityKey,isEndOfParagraph:isEndOfParagraph}}// cover edge cases where it doesn't find it
return{entityKey:entityKey,isEndOfParagraph:isEndOfParagraph}}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"renderBlockWithTimecodes",function(){return{component:_WrapperBlock.default,editable:!0,props:{showSpeakers:_this.state.showSpeakers,showTimecodes:_this.state.showTimecodes,timecodeOffset:_this.state.timecodeOffset,editorState:_this.state.editorState,setEditorNewContentState:_this.setEditorNewContentState,onWordClick:_this.props.onWordClick,handleAnalyticsEvents:_this.props.handleAnalyticsEvents}}}),_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)),"getCurrentWord",function(){var currentWord={start:"NA",end:"NA"};if(_this.state.transcriptData){var contentState=_this.state.editorState.getCurrentContent(),contentStateConvertEdToRaw=(0,_draftJs.convertToRaw)(contentState),entityMap=contentStateConvertEdToRaw.entityMap;// TODO: using convertToRaw here might be slowing down performance(?)
for(var entityKey in entityMap){var entity=entityMap[entityKey],word=entity.data;word.start<=_this.props.currentTime&&word.end>=_this.props.currentTime&&(currentWord.start=word.start,currentWord.end=word.end)}}if("NA"!==currentWord.start&&_this.props.isScrollIntoViewOn){var currentWordElement=document.querySelector("span.Word[data-start=\"".concat(currentWord.start,"\"]"));currentWordElement.scrollIntoView({block:"nearest",inline:"center"})}return currentWord}),_this.state={editorState:_draftJs.EditorState.createEmpty(),transcriptData:_this.props.transcriptData,isEditable:_this.props.isEditable,sttJsonType:_this.props.sttJsonType,timecodeOffset:_this.props.timecodeOffset,showSpeakers:_this.props.showSpeakers,showTimecodes:_this.props.showTimecodes,// inputCount: 0,
currentWord:{}},_this}return _inherits(TimedTextEditor,_React$Component),_createClass(TimedTextEditor,[{key:"componentDidMount",value:function componentDidMount(){this.loadData()}},{key:"componentDidUpdate",value:function componentDidUpdate(prevProps,prevState){prevState.transcriptData!==this.state.transcriptData&&this.loadData(),(prevState.timecodeOffset!==this.state.timecodeOffset||prevState.showSpeakers!==this.state.showSpeakers||prevState.showTimecodes!==this.state.showTimecodes)&&this.forceRenderDecorator()}},{key:"loadData",value:function loadData(){if(null!==this.props.transcriptData){var blocks=(0,_index.default)(this.props.transcriptData,this.props.sttJsonType);this.setEditorContentState(blocks)}}},{key:"getEditorContent",value:function getEditorContent(exportFormat){return(0,_index2.default)((0,_draftJs.convertToRaw)(this.state.editorState.getCurrentContent()),exportFormat||"draftjs")}// click on words - for navigation
// eslint-disable-next-line class-methods-use-this
},{key:"isPresentInLocalStorage",// eslint-disable-next-line class-methods-use-this
value:function isPresentInLocalStorage(mediaUrl){if(null!==mediaUrl){var mediaUrlName=mediaUrl;mediaUrl.includes("blob")&&(mediaUrlName=this.props.fileName);var data=localStorage.getItem("draftJs-".concat(mediaUrlName));return null!==data}return!1}},{key:"loadLocalSavedData",value:function loadLocalSavedData(mediaUrl){var mediaUrlName=mediaUrl;mediaUrl.includes("blob")&&(mediaUrlName=this.props.fileName);var data=JSON.parse(localStorage.getItem("draftJs-".concat(mediaUrlName)));if(null!==data){var lastLocalSavedDate=localStorage.getItem("timestamp-".concat(mediaUrlName));return this.setEditorContentState(data),lastLocalSavedDate}return""}// originally from
// https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-counter-plugin/src/WordCounter/index.js#L12
},{key:"render",value:function render(){var _this2=this,helpMessage=_react.default.createElement("div",{className:_indexModule.default.helpMessage},_react.default.createElement("span",null,_react.default.createElement(_reactFontawesome.FontAwesomeIcon,{className:_indexModule.default.icon,icon:_freeSolidSvgIcons.faMousePointer}),"Double click on a word or timestamp to jump to that point in the video."),_react.default.createElement("span",null,_react.default.createElement(_reactFontawesome.FontAwesomeIcon,{className:_indexModule.default.icon,icon:_freeSolidSvgIcons.faICursor}),"Start typing to edit text."),_react.default.createElement("span",null,_react.default.createElement(_reactFontawesome.FontAwesomeIcon,{className:_indexModule.default.icon,icon:_freeSolidSvgIcons.faUserEdit}),"You can add and change names of speakers in your transcript."),_react.default.createElement("span",null,_react.default.createElement(_reactFontawesome.FontAwesomeIcon,{className:_indexModule.default.icon,icon:_freeSolidSvgIcons.faKeyboard}),"Use keyboard shortcuts for quick control."),_react.default.createElement("span",null,_react.default.createElement(_reactFontawesome.FontAwesomeIcon,{className:_indexModule.default.icon,icon:_freeSolidSvgIcons.faSave}),"Save & export to get a copy to your desktop.")),tooltip=_react.default.createElement(_reactSimpleTooltip.default,{className:_indexModule.default.help,content:helpMessage,fadeDuration:250,fadeEasing:"ease-in",placement:"bottom",radius:5},_react.default.createElement(_reactFontawesome.FontAwesomeIcon,{className:_indexModule.default.icon,icon:_freeSolidSvgIcons.faQuestionCircle}),"How does this work?"),currentWord=this.getCurrentWord(),highlightColour="#69e3c2",unplayedColor="#767676",time=Math.round(4*this.props.currentTime)/4,editor=_react.default.createElement("section",{className:_indexModule.default.editor,onDoubleClick:function onDoubleClick(event){return _this2.handleDoubleClick(event)}},_react.default.createElement("style",{scoped:!0},"span.Word[data-start=\"".concat(currentWord.start,"\"] { background-color: ").concat(highlightColour,"; text-shadow: 0 0 0.01px black }"),"span.Word[data-start=\"".concat(currentWord.start,"\"]+span { background-color: ").concat(highlightColour," }"),"span.Word[data-prev-times~=\"".concat(Math.floor(time),"\"] { color: ").concat(unplayedColor," }"),"span.Word[data-prev-times~=\"".concat(time,"\"] { color: ").concat(unplayedColor," }"),"span.Word[data-confidence=\"low\"] { border-bottom: ".concat("1px dotted blue"," }")),_react.default.createElement(_draftJs.Editor,{editorState:this.state.editorState,onChange:this.onChange,stripPastedStyles:!0,blockRendererFn:this.renderBlockWithTimecodes,handleKeyCommand:function handleKeyCommand(command){return _this2.handleKeyCommand(command)},keyBindingFn:function keyBindingFn(e){return _this2.customKeyBindingFn(e)}}));return _react.default.createElement("section",null,tooltip,null===this.props.transcriptData?null:editor)}}],[{key:"getDerivedStateFromProps",value:function getDerivedStateFromProps(nextProps){return null===nextProps.transcriptData?null:{transcriptData:nextProps.transcriptData,isEditable:nextProps.isEditable,timecodeOffset:nextProps.timecodeOffset,showSpeakers:nextProps.showSpeakers,showTimecodes:nextProps.showTimecodes}}}]),TimedTextEditor}(_react.default.Component),getEntityStrategy=function(mutability){return function(contentBlock,callback,contentState){contentBlock.findEntityRanges(function(character){var entityKey=character.getEntity();return null!==entityKey&&contentState.getEntity(entityKey).getMutability()===mutability},callback)}},decorator=new _draftJs.CompositeDecorator([{strategy:getEntityStrategy("MUTABLE"),component:_Word.default}]);TimedTextEditor.propTypes={transcriptData:_propTypes.default.object,mediaUrl:_propTypes.default.string,isEditable:_propTypes.default.bool,onWordClick:_propTypes.default.func,sttJsonType:_propTypes.default.string,isPlaying:_propTypes.default.func,playMedia:_propTypes.default.func,currentTime:_propTypes.default.number,isScrollIntoViewOn:_propTypes.default.bool,isPauseWhileTypingOn:_propTypes.default.bool,timecodeOffset:_propTypes.default.number,handleAnalyticsEvents:_propTypes.default.func};var _default=TimedTextEditor;exports.default=_default;