diff options
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/apis/google_docs/GoogleApiClientUtils.ts | 189 | ||||
| -rw-r--r-- | src/client/util/DictationManager.ts | 66 | ||||
| -rw-r--r-- | src/client/views/DocumentDecorations.tsx | 7 | ||||
| -rw-r--r-- | src/client/views/GlobalKeyHandler.ts | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 114 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 6 |
6 files changed, 258 insertions, 126 deletions
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index 821c52270..798886def 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -1,4 +1,4 @@ -import { docs_v1 } from "googleapis"; +import { docs_v1, slides_v1 } from "googleapis"; import { PostToServer } from "../../../Utils"; import { RouteStore } from "../../../server/RouteStore"; import { Opt } from "../../../new_fields/Doc"; @@ -9,50 +9,84 @@ export const Pushes = "googleDocsPushCount"; export namespace GoogleApiClientUtils { - export namespace Docs { + export enum Service { + Documents = "Documents", + Slides = "Slides" + } - export enum Actions { - Create = "create", - Retrieve = "retrieve", - Update = "update" - } + export enum Actions { + Create = "create", + Retrieve = "retrieve", + Update = "update" + } - export enum WriteMode { - Insert, - Replace - } + export enum WriteMode { + Insert, + Replace + } - export type DocumentId = string; - export type Reference = DocumentId | CreateOptions; - export type TextContent = string | string[]; - export type IdHandler = (id: DocumentId) => any; + export type Identifier = string; + export type Reference = Identifier | CreateOptions; + export type TextContent = string | string[]; + export type IdHandler = (id: Identifier) => any; + export type CreationResult = Opt<Identifier>; + export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>; + export type ReadResult = { title?: string, body?: string }; - export type CreationResult = Opt<DocumentId>; - export type RetrievalResult = Opt<docs_v1.Schema$Document>; - export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>; - export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>; - export type ReadResult = { title?: string, body?: string }; + export interface CreateOptions { + service: Service; + title?: string; // if excluded, will use a default title annotated with the current date + } - export interface CreateOptions { - handler: IdHandler; // callback to process the documentId of the newly created Google Doc - title?: string; // if excluded, will use a default title annotated with the current date - } + export interface RetrieveOptions { + service: Service; + identifier: Identifier; + } - export interface RetrieveOptions { - documentId: DocumentId; - } + export interface ReadOptions { + identifier: Identifier; + removeNewlines?: boolean; + } - export type ReadOptions = RetrieveOptions & { removeNewlines?: boolean }; + export interface WriteOptions { + mode: WriteMode; + content: TextContent; + reference: Reference; + index?: number; // if excluded, will compute the last index of the document and append the content there + } - export interface WriteOptions { - mode: WriteMode; - content: TextContent; - reference: Reference; - index?: number; // if excluded, will compute the last index of the document and append the content there + /** + * After following the authentication routine, which connects this API call to the current signed in account + * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which + * should appear in the user's Google Doc library instantaneously. + * + * @param options the title to assign to the new document, and the information necessary + * to store the new documentId returned from the creation process + * @returns the documentId of the newly generated document, or undefined if the creation process fails. + */ + export const create = async (options: CreateOptions): Promise<CreationResult> => { + const path = `${RouteStore.googleDocs}/${options.service}/${Actions.Create}`; + const parameters = { + requestBody: { + title: options.title || `Dash Export (${new Date().toDateString()})` + } + }; + try { + const schema: any = await PostToServer(path, parameters); + let key = ["document", "presentation"].find(prefix => `${prefix}Id` in schema) + "Id"; + return schema[key]; + } catch { + return undefined; } + }; + + export namespace Docs { + + export type RetrievalResult = Opt<docs_v1.Schema$Document | slides_v1.Schema$Presentation>; + export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>; export interface UpdateOptions { - documentId: DocumentId; + documentId: Identifier; requests: docs_v1.Schema$Request[]; } @@ -96,46 +130,27 @@ export namespace GoogleApiClientUtils { } - /** - * After following the authentication routine, which connects this API call to the current signed in account - * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which - * should appear in the user's Google Doc library instantaneously. - * - * @param options the title to assign to the new document, and the information necessary - * to store the new documentId returned from the creation process - * @returns the documentId of the newly generated document, or undefined if the creation process fails. - */ - export const create = async (options: CreateOptions): Promise<CreationResult> => { - const path = RouteStore.googleDocs + Actions.Create; - const parameters = { - requestBody: { - title: options.title || `Dash Export (${new Date().toDateString()})` - } - }; - try { - const schema: docs_v1.Schema$Document = await PostToServer(path, parameters); - const generatedId = schema.documentId; - if (generatedId) { - options.handler(generatedId); - return generatedId; - } - } catch { - return undefined; - } - }; + const KeyMapping = new Map<Service, string>([ + [Service.Documents, "documentId"], + [Service.Slides, "presentationId"] + ]); export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => { - const path = RouteStore.googleDocs + Actions.Retrieve; + const path = `${RouteStore.googleDocs}/${options.service}/${Actions.Retrieve}`; try { - const schema: RetrievalResult = await PostToServer(path, options); - return schema; + let parameters: any = {}, key: string | undefined; + if ((key = KeyMapping.get(options.service))) { + parameters[key] = options.identifier; + const schema: RetrievalResult = await PostToServer(path, parameters); + return schema; + } } catch { return undefined; } }; export const update = async (options: UpdateOptions): Promise<UpdateResult> => { - const path = RouteStore.googleDocs + Actions.Update; + const path = `${RouteStore.googleDocs}/${Service.Documents}/${Actions.Update}`; const parameters = { documentId: options.documentId, requestBody: { @@ -151,7 +166,7 @@ export namespace GoogleApiClientUtils { }; export const read = async (options: ReadOptions): Promise<ReadResult> => { - return retrieve(options).then(document => { + return retrieve({ ...options, service: Service.Documents }).then(document => { let result: ReadResult = {}; if (document) { let title = document.title; @@ -163,7 +178,7 @@ export namespace GoogleApiClientUtils { }; export const readLines = async (options: ReadOptions): Promise<ReadLinesResult> => { - return retrieve(options).then(document => { + return retrieve({ ...options, service: Service.Documents }).then(document => { let result: ReadLinesResult = {}; if (document) { let title = document.title; @@ -177,14 +192,14 @@ export namespace GoogleApiClientUtils { export const write = async (options: WriteOptions): Promise<UpdateResult> => { const requests: docs_v1.Schema$Request[] = []; - const documentId = await Utils.initialize(options.reference); - if (!documentId) { + const identifier = await Utils.initialize(options.reference); + if (!identifier) { return undefined; } let index = options.index; const mode = options.mode; if (!(index && mode === WriteMode.Insert)) { - let schema = await retrieve({ documentId }); + let schema = await retrieve({ identifier, service: Service.Documents }); if (!schema || !(index = Utils.endOf(schema))) { return undefined; } @@ -210,7 +225,7 @@ export namespace GoogleApiClientUtils { if (!requests.length) { return undefined; } - let replies: any = await update({ documentId, requests }); + let replies: any = await update({ documentId: identifier, requests }); let errors = "errors"; if (errors in replies) { console.log("Write operation failed:"); @@ -221,4 +236,36 @@ export namespace GoogleApiClientUtils { } + export namespace Slides { + + export namespace Utils { + + export const extractTextBoxes = (slides: slides_v1.Schema$Page[]) => { + slides.map(slide => { + let elements = slide.pageElements; + if (elements) { + let textboxes: slides_v1.Schema$TextContent[] = []; + for (let element of elements) { + if (element && element.shape && element.shape.shapeType === "TEXT_BOX" && element.shape.text) { + textboxes.push(element.shape.text); + } + } + textboxes.map(text => { + if (text.textElements) { + text.textElements.map(element => { + + }); + } + if (text.lists) { + + } + }); + } + }); + }; + + } + + } + }
\ No newline at end of file diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 488a146bf..fb3c15cea 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -45,7 +45,7 @@ export namespace DictationManager { export namespace Controls { - const infringe = "unable to process: dictation manager still involved in previous session"; + export const Infringed = "unable to process: dictation manager still involved in previous session"; const intraSession = ". "; const interSession = " ... "; @@ -64,35 +64,45 @@ export namespace DictationManager { export type ListeningUIStatus = { interim: boolean } | false; export interface ListeningOptions { + useOverlay: boolean; language: string; continuous: ContinuityArgs; delimiters: DelimiterArgs; interimHandler: InterimResultHandler; tryExecute: boolean; + terminators: string[]; } export const listen = async (options?: Partial<ListeningOptions>) => { let results: string | undefined; let main = MainView.Instance; - main.dictationOverlayVisible = true; - main.isListening = { interim: false }; + let overlay = options !== undefined && options.useOverlay; + if (overlay) { + main.dictationOverlayVisible = true; + main.isListening = { interim: false }; + } try { results = await listenImpl(options); if (results) { Utils.CopyText(results); - main.isListening = false; - let execute = options && options.tryExecute; - main.dictatedPhrase = execute ? results.toLowerCase() : results; - main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true; + if (overlay) { + main.isListening = false; + let execute = options && options.tryExecute; + main.dictatedPhrase = execute ? results.toLowerCase() : results; + main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true; + } + options && options.tryExecute && await DictationManager.Commands.execute(results); } } catch (e) { - main.isListening = false; - main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`; - main.dictationSuccess = false; + if (overlay) { + main.isListening = false; + main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`; + main.dictationSuccess = false; + } } finally { - main.initiateDictationFade(); + overlay && main.initiateDictationFade(); } return results; @@ -100,7 +110,7 @@ export namespace DictationManager { const listenImpl = (options?: Partial<ListeningOptions>) => { if (isListening) { - return infringe; + return Infringed; } isListening = true; @@ -128,6 +138,12 @@ export namespace DictationManager { recognizer.onresult = (e: SpeechRecognitionEvent) => { current = synthesize(e, intra); + let matchedTerminator: string | undefined; + if (options && options.terminators && (matchedTerminator = options.terminators.find(end => current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false))) { + current = matchedTerminator; + recognizer.abort(); + return complete(); + } handler && handler(current); isManuallyStopped && complete(); }; @@ -163,13 +179,13 @@ export namespace DictationManager { } isManuallyStopped = true; salvageSession ? recognizer.stop() : recognizer.abort(); - let main = MainView.Instance; - if (main.dictationOverlayVisible) { - main.cancelDictationFade(); - main.dictationOverlayVisible = false; - main.dictationSuccess = undefined; - setTimeout(() => main.dictatedPhrase = placeholder, 500); - } + // let main = MainView.Instance; + // if (main.dictationOverlayVisible) { + // main.cancelDictationFade(); + // main.dictationOverlayVisible = false; + // main.dictationSuccess = undefined; + // setTimeout(() => main.dictatedPhrase = placeholder, 500); + // } }; const synthesize = (e: SpeechRecognitionEvent, delimiter?: string) => { @@ -301,11 +317,16 @@ export namespace DictationManager { } }], - ["create bulleted note", { + ["new outline", { action: (target: DocumentView) => { let newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" }); + newBox.autoHeight = true; let proto = newBox.proto!; - let proseMirrorState = '"{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":""}]}]}]}]},"selection":{"type":"text","anchor":1,"head":1}}"'; + proto.page = -1; + let prompt = "Press alt + r to start dictating here..."; + let head = 3; + let anchor = head + prompt.length; + let proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`; proto.data = new RichTextField(proseMirrorState); proto.backgroundColor = "#eeffff"; target.props.addDocTab(newBox, proto, "onRight"); @@ -323,6 +344,9 @@ export namespace DictationManager { let what = matches[2]; let dataDoc = Doc.GetProto(target.props.Document); let fieldKey = "data"; + if (isNaN(count)) { + return; + } for (let i = 0; i < count; i++) { let created: Doc | undefined; switch (what) { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 891fd7847..a28088032 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -714,7 +714,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; let dataDoc = Doc.GetProto(this.targetDoc); if (!canPull || !dataDoc[GoogleRef]) return (null); - let icon = !dataDoc.unchanged ? (this.pullIcon as any) : fetch; + let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; icon = this.openHover ? "share" : icon; let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`; @@ -727,10 +727,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> backgroundColor: this.pullColor, transition: "0.2s ease all" }} - onPointerEnter={e => e.ctrlKey && runInAction(() => this.openHover = true)} + onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)} onPointerLeave={() => runInAction(() => this.openHover = false)} onClick={e => { - if (e.ctrlKey) { + if (e.altKey) { + e.preventDefault(); window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`); } else { this.clearPullColor(); diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 8a7295c65..d0464bd5f 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -105,7 +105,7 @@ export default class KeyManager { switch (keyname) { case " ": - DictationManager.Controls.listen({ tryExecute: true }); + DictationManager.Controls.listen({ useOverlay: true, tryExecute: true }); stopPropagation = true; preventDefault = true; } diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 44a481953..5492a457d 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -6,7 +6,7 @@ import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; import { Fragment, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model"; -import { EditorState, Plugin, Transaction } from "prosemirror-state"; +import { EditorState, Plugin, Transaction, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../new_fields/DateField'; import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc"; @@ -35,6 +35,8 @@ import React = require("react"); import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; import { DocumentDecorations } from '../DocumentDecorations'; import { MainOverlayTextBox } from '../MainOverlayTextBox'; +import { DictationManager } from '../../util/DictationManager'; +import { ReplaceStep } from 'prosemirror-transform'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -62,7 +64,7 @@ export const GoogleRef = "googleDocId"; type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); -type PullHandler = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => void; +type PullHandler = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => void; @observer export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { @@ -85,7 +87,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private pushReactionDisposer: Opt<IReactionDisposer>; private dropDisposer?: DragManager.DragDropDisposer; public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } - private isGoogleDocsUpdate = false; @observable _entered = false; @observable public static InputBoxOverlay?: FormattedTextBox = undefined; @@ -180,6 +181,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe const marks = tx.storedMarks; if (marks) { FormattedTextBox._toolTipTextMenu.mark_key_pressed(marks); } } + this._applyingChange = true; const fieldkey = "preview"; if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"); @@ -265,6 +267,64 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + recordKeyHandler = (e: KeyboardEvent) => { + if (this.props.Document !== SelectionManager.SelectedDocuments()[0].props.Document) { + return; + } + if (e.key === "R" && e.altKey) { + e.stopPropagation(); + e.preventDefault(); + this.recordBullet(); + } + } + + recordBullet = async () => { + let completedCue = "end session"; + let results = await DictationManager.Controls.listen({ + interimHandler: this.setCurrentBulletContent, + continuous: { indefinite: false }, + terminators: [completedCue, "bullet", "next"] + }); + if (results && [DictationManager.Controls.Infringed, completedCue].includes(results)) { + DictationManager.Controls.stop(); + return; + } + this.nextBullet(this._editorView!.state.selection.to); + setTimeout(this.recordBullet, 2000); + } + + setCurrentBulletContent = (value: string) => { + if (this._editorView) { + let state = this._editorView.state; + let from = state.selection.from; + let to = state.selection.to; + this._editorView.dispatch(state.tr.insertText(value, from, to)); + state = this._editorView.state; + let updated = TextSelection.create(state.doc, from, from + value.length); + this._editorView.dispatch(state.tr.setSelection(updated)); + } + } + + nextBullet = (pos: number) => { + if (this._editorView) { + let frag = Fragment.fromArray(this.newListItems(2)); + let slice = new Slice(frag, 2, 2); + let state = this._editorView.state; + this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice))); + pos += 4; + state = this._editorView.state; + this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos))); + } + } + + private newListItems = (count: number) => { + let listItems: any[] = []; + for (let i = 0; i < count; i++) { + listItems.push(schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create())); + } + return listItems; + } + componentDidMount() { const config = { schema, @@ -298,7 +358,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } this.pullFromGoogleDoc(this.checkState); - runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true); + this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true); this._reactionDisposer = reaction( () => { @@ -309,13 +369,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this._editorView && !this._applyingChange) { let updatedState = JSON.parse(incomingValue); this._editorView.updateState(EditorState.fromJSON(config, updatedState)); - // manually sets cursor selection at the end of the text on focus - if (this.isGoogleDocsUpdate) { - this.isGoogleDocsUpdate = false; - let end = this._editorView.state.doc.content.size - 1; - updatedState.selection = { type: "text", anchor: end, head: end }; - this._editorView.updateState(EditorState.fromJSON(config, updatedState)); - } this.tryUpdateHeight(); } } @@ -378,22 +431,20 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } pushToGoogleDoc = async () => { - this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => { - let modes = GoogleApiClientUtils.Docs.WriteMode; + this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { + let modes = GoogleApiClientUtils.WriteMode; let mode = modes.Replace; - let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string"); + let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string"); if (!reference) { mode = modes.Insert; - reference = { - title: StrCast(this.dataDoc.title), - handler: id => this.dataDoc[GoogleRef] = id - }; + reference = { service: GoogleApiClientUtils.Service.Documents, title: StrCast(this.dataDoc.title) }; } let redo = async () => { let data = Cast(this.dataDoc.data, RichTextField); if (this._editorView && reference && data) { let content = data[ToPlainText](); let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); + response && (this.dataDoc[GoogleRef] = response.documentId); let pushSuccess = response !== undefined && !("errors" in response); dataDoc.unchanged = pushSuccess; DocumentDecorations.Instance.startPushOutcome(pushSuccess); @@ -413,32 +464,38 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe pullFromGoogleDoc = async (handler: PullHandler) => { let dataDoc = this.dataDoc; let documentId = StrCast(dataDoc[GoogleRef]); - let exportState: GoogleApiClientUtils.Docs.ReadResult = {}; + let exportState: GoogleApiClientUtils.ReadResult = {}; if (documentId) { - exportState = await GoogleApiClientUtils.Docs.read({ documentId }); + exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId }); } UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } - updateState = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => { + updateState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { let pullSuccess = false; if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) { - let data = Cast(dataDoc.data, RichTextField); - if (data) { + const data = Cast(dataDoc.data, RichTextField); + if (data instanceof RichTextField) { pullSuccess = true; - this.isGoogleDocsUpdate = true; dataDoc.data = new RichTextField(data[FromPlainText](exportState.body)); + setTimeout(() => { + if (this._editorView) { + let state = this._editorView.state; + let end = state.doc.content.size - 1; + this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end))); + } + }, 0); dataDoc.title = exportState.title; + this.Document.customTitle = true; dataDoc.unchanged = true; } } else { delete dataDoc[GoogleRef]; } DocumentDecorations.Instance.startPullOutcome(pullSuccess); - this.tryUpdateHeight(); } - checkState = (exportState: GoogleApiClientUtils.Docs.ReadResult, dataDoc: Doc) => { + checkState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) { let data = Cast(dataDoc.data, RichTextField); if (data) { @@ -573,7 +630,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (!this.props.isOverlay) this.props.select(false); else this._editorView!.focus(); } - this.tryUpdateHeight(); } componentWillUnmount() { @@ -659,6 +715,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @action onFocused = (e: React.FocusEvent): void => { + document.removeEventListener("keypress", this.recordKeyHandler); + document.addEventListener("keypress", this.recordKeyHandler); + this.tryUpdateHeight(); if (!this.props.isOverlay) { FormattedTextBox.InputBoxOverlay = this; } else { @@ -708,6 +767,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }); } onBlur = (e: any) => { + document.removeEventListener("keypress", this.recordKeyHandler); if (this._undoTyping) { this._undoTyping.end(); this._undoTyping = undefined; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 704030d85..3f4ee8960 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -34,7 +34,7 @@ library.add(faVideo); export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) { private _reactionDisposer?: IReactionDisposer; private _youtubeReactionDisposer?: IReactionDisposer; - private _youtubePlayer: any = undefined; + private _youtubePlayer: YT.Player | undefined = undefined; private _videoRef: HTMLVideoElement | null = null; private _youtubeIframeId: number = -1; private _youtubeContentCreated = false; @@ -78,7 +78,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD @action public Pause = (update: boolean = true) => { this.Playing = false; update && this.player && this.player.pause(); - update && this._youtubePlayer && this._youtubePlayer.pauseVideo(); + update && this._youtubePlayer && this._youtubePlayer.pauseVideo && this._youtubePlayer.pauseVideo(); this._youtubePlayer && this._playTimer && clearInterval(this._playTimer); this._playTimer = undefined; this.updateTimecode(); @@ -244,7 +244,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let onYoutubePlayerStateChange = (event: any) => runInAction(() => { if (started && event.data === YT.PlayerState.PLAYING) { started = false; - this._youtubePlayer.unMute(); + this._youtubePlayer && this._youtubePlayer.unMute(); this.Pause(); return; } |
