diff options
-rw-r--r-- | src/client/documents/DocumentTypes.ts | 3 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 9 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 27 | ||||
-rw-r--r-- | src/client/views/nodes/ScreenshotBox.scss | 46 | ||||
-rw-r--r-- | src/client/views/nodes/ScreenshotBox.tsx | 193 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 17 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.scss | 7 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 9 | ||||
-rw-r--r-- | src/server/authentication/models/current_user_utils.ts | 3 |
11 files changed, 296 insertions, 23 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index e3ce8e798..5ec1cfdb4 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -27,5 +27,6 @@ export enum DocumentType { DOCULINK = "doculink", PDFANNO = "pdfanno", INK = "ink", - DOCUMENT = "document" + DOCUMENT = "document", + SCREENSHOT = "screenshot", }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 131a48a2b..b5cffaddd 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -58,6 +58,7 @@ import { MessageStore } from "../../server/Message"; import { ContextMenuProps } from "../views/ContextMenuItem"; import { ContextMenu } from "../views/ContextMenu"; import { LinkBox } from "../views/nodes/LinkBox"; +import { ScreenshotBox } from "../views/nodes/ScreenshotBox"; const requestImageSize = require('../util/request-image-size'); const path = require('path'); @@ -277,6 +278,10 @@ export namespace Docs { [DocumentType.INK, { layout: { view: InkingStroke, dataField: data }, options: { backgroundColor: "transparent" } + }], + [DocumentType.SCREENSHOT, { + layout: { view: ScreenshotBox, dataField: data }, + options: {} }] ]); @@ -522,6 +527,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.WEBCAM), "", options); } + export function ScreenshotDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), "", options); + } + export function AudioDocument(url: string, options: DocumentOptions = {}) { const instance = InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options); Doc.GetProto(instance).backgroundColor = ComputedField.MakeFunction("this._audioState === 'playing' ? 'green':'gray'"); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 2d6777a4e..97177855f 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -316,7 +316,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { const item = e.dataTransfer.items[i]; if (item.kind === "string" && item.type.includes("uri")) { const stringContents = await new Promise<string>(resolve => item.getAsString(resolve)); - const type = (await rp.head(Utils.CorsProxy(stringContents)))["content-type"]; + const type = "html";// (await rp.head(Utils.CorsProxy(stringContents)))["content-type"]; if (type) { const doc = await Docs.Get.DocumentFromType(type, stringContents, options); doc && generatedDocuments.push(doc); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index f121ba3e0..239f414fd 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -29,6 +29,7 @@ import { ColorBox } from "./ColorBox"; import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo"; import { DocuLinkBox } from "./DocuLinkBox"; import { PresElementBox } from "../presentationview/PresElementBox"; +import { ScreenshotBox } from "./ScreenshotBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import { InkingStroke } from "../InkingStroke"; @@ -99,7 +100,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, PresElementBox, QueryBox, ColorBox, DashWebRTCVideo, DocuLinkBox, InkingStroke, DocumentBox, LinkBox, - RecommendationsBox, + RecommendationsBox, ScreenshotBox }} bindings={this.CreateBindings()} jsx={this.layout} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index f870a5df4..b1c172e22 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,37 +1,34 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; -import { faAsterisk, faFileAudio, faImage, faPaintBrush, faBrain } from '@fortawesome/free-solid-svg-icons'; +import { faAsterisk, faBrain, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction, trace } from 'mobx'; +import { action, computed, observable, runInAction } from 'mobx'; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym, DataSym } from '../../../new_fields/Doc'; +import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc'; +import { documentSchema } from '../../../new_fields/documentSchemas'; +import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; +import { ObjectField } from '../../../new_fields/ObjectField'; import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; import { ComputedField } from '../../../new_fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; import { AudioField, ImageField } from '../../../new_fields/URLField'; -import { Utils, returnOne, emptyFunction } from '../../../Utils'; +import { TraceMobx } from '../../../new_fields/util'; +import { emptyFunction, returnOne, Utils } from '../../../Utils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs } from '../../documents/Documents'; +import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; +import { SelectionManager } from '../../util/SelectionManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../../views/ContextMenu"; +import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenuProps } from '../ContextMenuItem'; import { DocAnnotatableComponent } from '../DocComponent'; import FaceRectangles from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); -import { SearchUtil } from '../../util/SearchUtil'; -import { ClientRecommender } from '../../ClientRecommender'; -import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; -import { documentSchema } from '../../../new_fields/documentSchemas'; -import { Id, Copy } from '../../../new_fields/FieldSymbols'; -import { TraceMobx } from '../../../new_fields/util'; -import { SelectionManager } from '../../util/SelectionManager'; -import { cache } from 'sharp'; -import { ObjectField } from '../../../new_fields/ObjectField'; -import { Networking } from '../../Network'; const requestImageSize = require('../../util/request-image-size'); const path = require('path'); const { Howl } = require('howler'); @@ -163,7 +160,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" }); funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" }); funcs.push({ - description: "Reset Native Dimensions", event: action(() => { + description: "Reset Native Dimensions", event: action(async () => { const curNW = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"]); const curNH = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"]); if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) { diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss new file mode 100644 index 000000000..4aaccb472 --- /dev/null +++ b/src/client/views/nodes/ScreenshotBox.scss @@ -0,0 +1,46 @@ +.screenshotBox { + pointer-events: all; + transform-origin: top left; + background: white; + color: black; + // .screenshotBox-viewer { + // opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger + // } + // .inkingCanvas-paths-markers { + // opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround + // } +} + +.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-cont-fullScreen { + width: 100%; + z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt + position: absolute; +} + +.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-content-fullScreen { + height: Auto; +} + +.screenshotBox-content-interactive, .screenshotBox-content-fullScreen { + pointer-events: all; +} + +.screenshotBox-snapshot{ + color : white; + top :25px; + right : 25px; + position: absolute; + background-color:rgba(50, 50, 50, 0.2); + transform-origin: left top; + pointer-events:all; +} + +.screenshotBox-recorder{ + color : white; + top :25px; + right : 50px; + position: absolute; + background-color:rgba(50, 50, 50, 0.2); + transform-origin: left top; + pointer-events:all; +} diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx new file mode 100644 index 000000000..de9e521f6 --- /dev/null +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -0,0 +1,193 @@ +import React = require("react"); +import { library } from "@fortawesome/fontawesome-svg-core"; +import { faVideo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, IReactionDisposer, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as rp from 'request-promise'; +import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas"; +import { makeInterface } from "../../../new_fields/Schema"; +import { ScriptField } from "../../../new_fields/ScriptField"; +import { Cast, StrCast } from "../../../new_fields/Types"; +import { VideoField } from "../../../new_fields/URLField"; +import { emptyFunction, returnFalse, returnOne, Utils } from "../../../Utils"; +import { Docs, DocUtils } from "../../documents/Documents"; +import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { DocAnnotatableComponent } from "../DocComponent"; +import { InkingControl } from "../InkingControl"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./ScreenshotBox.scss"; +const path = require('path'); + +type ScreenshotDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>; +const ScreenshotDocument = makeInterface(documentSchema, positionSchema); + +library.add(faVideo); + +@observer +export class ScreenshotBox extends DocAnnotatableComponent<FieldViewProps, ScreenshotDocument>(ScreenshotDocument) { + private _reactionDisposer?: IReactionDisposer; + private _videoRef: HTMLVideoElement | null = null; + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); } + + public get player(): HTMLVideoElement | null { + return this._videoRef; + } + + videoLoad = () => { + const aspect = this.player!.videoWidth / this.player!.videoHeight; + const nativeWidth = (this.Document._nativeWidth || 0); + const nativeHeight = (this.Document._nativeHeight || 0); + if (!nativeWidth || !nativeHeight) { + if (!this.Document._nativeWidth) this.Document._nativeWidth = this.player!.videoWidth; + this.Document._nativeHeight = (this.Document._nativeWidth || 0) / aspect; + this.Document._height = (this.Document._width || 0) / aspect; + } + if (!this.Document.duration) this.Document.duration = this.player!.duration; + } + + @action public Snapshot() { + const width = this.Document._width || 0; + const height = this.Document._height || 0; + const canvas = document.createElement('canvas'); + canvas.width = 640; + canvas.height = 640 * (this.Document._nativeHeight || 0) / (this.Document._nativeWidth || 1); + const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions + if (ctx) { + ctx.rect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "blue"; + ctx.fill(); + this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); + } + + if (this._videoRef) { + //convert to desired file format + const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' + // if you want to preview the captured image, + const filename = path.basename(encodeURIComponent("screenshot" + Utils.GenerateGuid().replace(/\..*$/, "").replace(" ", "_"))); + ScreenshotBox.convertDataUri(dataUrl, filename).then(returnedFilename => { + setTimeout(() => { + if (returnedFilename) { + const imageSummary = Docs.Create.ImageDocument(Utils.prepend(returnedFilename), { + x: (this.Document.x || 0) + width, y: (this.Document.y || 0), + _width: 150, _height: height / width * 150, title: "--screenshot--" + }); + this.props.addDocument?.(imageSummary); + } + }, 500); + }); + } + } + + componentDidMount() { + } + + componentWillUnmount() { + this._reactionDisposer && this._reactionDisposer(); + } + + @action + setVideoRef = (vref: HTMLVideoElement | null) => { + this._videoRef = vref; + } + + public static async convertDataUri(imageUri: string, returnedFilename: string) { + try { + const posting = Utils.prepend("/uploadURI"); + const returnedUri = await rp.post(posting, { + body: { + uri: imageUri, + name: returnedFilename + }, + json: true, + }); + return returnedUri; + + } catch (e) { + console.log(e); + } + } + @observable _screenCapture = false; + specificContextMenu = (e: React.MouseEvent): void => { + const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); + if (field) { + const url = field.url.href; + const subitems: ContextMenuProps[] = []; + subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" }); + subitems.push({ + description: "Screen Capture", event: (async () => { + runInAction(() => this._screenCapture = !this._screenCapture); + this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + }), icon: "expand-arrows-alt" + }); + ContextMenu.Instance.addItem({ description: "Screenshot Funcs...", subitems: subitems, icon: "video" }); + } + } + + @computed get content() { + const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive"; + const style = "videoBox-content" + interactive; + return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} + style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} + onCanPlay={this.videoLoad} + controls={true} + onClick={e => e.preventDefault()}> + <source type="video/mp4" /> + Not supported. + </video>; + } + + toggleRecording = action(async () => { + this._screenCapture = !this._screenCapture; + this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + }) + + private get uIButtons() { + return ([ + <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} > + <FontAwesomeIcon icon="file" size="lg" /> + </div>, + <div className="screenshotBox-snapshot" key="snap" onPointerDown={this.onSnapshot} > + <FontAwesomeIcon icon="camera" size="lg" /> + </div>]); + } + + onSnapshot = (e: React.PointerEvent) => { + this.Snapshot(); + e.stopPropagation(); + e.preventDefault(); + } + + + contentFunc = () => [this.content]; + render() { + return (<div className="videoBox" onContextMenu={this.specificContextMenu} + style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > + <div className="videoBox-viewer" > + <CollectionFreeFormView {...this.props} + PanelHeight={this.props.PanelHeight} + PanelWidth={this.props.PanelWidth} + annotationsKey={this.annotationKey} + focus={this.props.focus} + isSelected={this.props.isSelected} + isAnnotationOverlay={true} + select={emptyFunction} + active={this.annotationsActive} + ContentScaling={returnOne} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={returnFalse} + CollectionView={undefined} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc}> + {this.contentFunc} + </CollectionFreeFormView> + </div> + {this.uIButtons} + </div >); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 439f2d85f..24b66d8f7 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -195,6 +195,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum console.log(e); } } + @observable _screenCapture = false; specificContextMenu = (e: React.MouseEvent): void => { const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); if (field) { @@ -203,6 +204,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" }); subitems.push({ description: "Toggle Show Controls", event: action(() => VideoBox._showControls = !VideoBox._showControls), icon: "expand-arrows-alt" }); subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" }); + subitems.push({ + description: "Screen Capture", event: (async () => { + runInAction(() => this._screenCapture = !this._screenCapture); + this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + }), icon: "expand-arrows-alt" + }); ContextMenu.Instance.addItem({ description: "Video Funcs...", subitems: subitems, icon: "video" }); } } @@ -212,8 +219,14 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive"; const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div>Loading</div> : - <video className={`${style}`} key="video" ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls} - onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} onClick={e => e.preventDefault()}> + <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} + style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} + onCanPlay={this.videoLoad} + controls={VideoBox._showControls} + onPlay={() => this.Play()} + onSeeked={this.updateTimecode} + onPause={() => this.Pause()} + onClick={e => e.preventDefault()}> <source src={field.url.href} type="video/mp4" /> Not supported. </video>; diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index 226103a53..b41687c11 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -1,13 +1,18 @@ @import "../globalCssVariables.scss"; + +.webBox-container, .webBox-container-dragging { + transform-origin: top left; +} .webBox-cont, -.webBox-cont-interactive { +.webBox-cont-dragging { padding: 0vw; position: absolute; top: 0; left: 0; width: 100%; height: 100%; + transform-origin: top left; overflow: auto; pointer-events: none; } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 557adfc03..591864f2c 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -323,7 +323,14 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> </>); } render() { - return (<div className={"webBox-container"} > + const dragging = "";//</div>!SelectionManager.GetIsDragging() ? "" : "-dragging"; + return (<div className={`webBox-container${dragging}`} + style={{ + transform: `scale(${this.props.ContentScaling()})`, + width: `${100 / this.props.ContentScaling()}%`, + height: `${100 / this.props.ContentScaling()}%`, + pointerEvents: this.props.Document.isBackground ? "none" : undefined + }} > <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index d1f68ba49..ea7b91b23 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -67,7 +67,8 @@ export class CurrentUserUtils { { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", {_width: 300, _height: 300, title: "New Webpage" })' }, { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 200, title: "an image of a cat" })' }, { title: "buxton", icon: "cloud-upload-alt", ignoreClick: true, drag: "Docs.Create.Buxton()" }, - { title: "webcam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { width: 400, height: 400, title: "a test cam" })' }, + { title: "screenshot", icon: "frame", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' }, + { title: "webcam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' }, { title: "record", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` }, { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: "Button" })' }, { title: "presentation", icon: "tv", click: 'openOnRight(Doc.UserDoc().curPresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().curPresentation = getCopy(this.dragFactory,true)`, dragFactory: emptyPresentation }, |