From 0178de4ab9ffd11630b700f9c02468b74beabd14 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 17 Aug 2022 12:31:26 -0400 Subject: fixed dragging docs on web and pdf to work better by temporarily adding transparent docs to the opaque layer so they can get drop events. cleaned eraser and pen interaction code and made erasing strokes work faster and avoid hanging by not intersecting strokes that are already partially deleted. --- src/client/views/nodes/VideoBox.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index a3d501153..0ff15f93b 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -831,16 +831,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent 1) { - return { height: '100%' }; - } else { - return { width: '100%' }; + //prettier-ignore + return ({ height: '100%' }); } + //prettier-ignore + return ({ width: '100%' }); } // for zoom slider, sets timeline waveform zoom - zoom = (zoom: number) => { - this.timeline?.setZoom(zoom); - }; + zoom = (zoom: number) => this.timeline?.setZoom(zoom); // plays link playLink = (doc: Doc) => { -- cgit v1.2.3-70-g09d2 From 5ad8289e8ca3b6ff8304ac4fa3c2d3adaa9abf6d Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 1 Sep 2022 14:52:04 -0400 Subject: afixed loadingBox to show error message for unsupported videos. fixed video uploading to generate errors for unsupported (Chrome) codecs. fixed videos that don't have codec support to still play audio and show duration. fixed dragging rotated documents to show and place the rotted document WYSIWYG --- src/client/Network.ts | 97 +++++++------ src/client/documents/Documents.ts | 7 +- src/client/util/DragManager.ts | 44 ++++-- src/client/views/nodes/DocumentView.tsx | 4 +- src/client/views/nodes/LoadingBox.scss | 2 + src/client/views/nodes/LoadingBox.tsx | 4 +- src/client/views/nodes/VideoBox.tsx | 10 +- src/server/DashUploadUtils.ts | 236 +++++++++++++++++--------------- 8 files changed, 225 insertions(+), 179 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/Network.ts b/src/client/Network.ts index c781d4b6b..a222b320f 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -1,57 +1,54 @@ -import { Utils } from "../Utils"; +import { Utils } from '../Utils'; import requestPromise = require('request-promise'); -import { Upload } from "../server/SharedMediaTypes"; +import { Upload } from '../server/SharedMediaTypes'; export namespace Networking { + export async function FetchFromServer(relativeRoute: string) { + return (await fetch(relativeRoute)).text(); + } - export async function FetchFromServer(relativeRoute: string) { - return (await fetch(relativeRoute)).text(); - } + export async function PostToServer(relativeRoute: string, body?: any) { + const options = { + uri: Utils.prepend(relativeRoute), + method: 'POST', + body, + json: true, + }; + return requestPromise.post(options); + } - export async function PostToServer(relativeRoute: string, body?: any) { - const options = { - uri: Utils.prepend(relativeRoute), - method: "POST", - body, - json: true - }; - return requestPromise.post(options); - } - - /** - * Handles uploading basic file types to server and makes the API call to "/uploadFormData" endpoint - * with the mapping of GUID to filem as parameters. - * - * @param files the files to be uploaded to the server - * @returns the response as a json from the server - */ - export async function UploadFilesToServer(files: File | File[]): Promise[]> { - const formData = new FormData(); - if (Array.isArray(files)) { - if (!files.length) { - return []; - } - files.forEach(file => formData.append(Utils.GenerateGuid(), file)); - } else { - formData.append(Utils.GenerateGuid(), files); + /** + * Handles uploading basic file types to server and makes the API call to "/uploadFormData" endpoint + * with the mapping of GUID to filem as parameters. + * + * @param files the files to be uploaded to the server + * @returns the response as a json from the server + */ + export async function UploadFilesToServer(files: File | File[]): Promise[]> { + const formData = new FormData(); + if (Array.isArray(files)) { + if (!files.length) { + return []; } - const parameters = { - method: 'POST', - body: formData - }; - const response = await fetch("/uploadFormData", parameters); - return response.json(); - } - - export async function UploadYoutubeToServer(videoId: string): Promise[]> { - const parameters = { - method: 'POST', - body: JSON.stringify({ videoId }), - json: true - }; - const response = await fetch("/uploadYoutubeVideo", parameters); - return response.json(); - } - + files.forEach(file => formData.append(Utils.GenerateGuid(), file)); + } else { + formData.append(Utils.GenerateGuid(), files); + } + const parameters = { + method: 'POST', + body: formData, + }; + const response = await fetch('/uploadFormData', parameters); + return response.json(); + } -} \ No newline at end of file + export async function UploadYoutubeToServer(videoId: string): Promise[]> { + const parameters = { + method: 'POST', + body: JSON.stringify({ videoId }), + json: true, + }; + const response = await fetch('/uploadYoutubeVideo', parameters); + return response.json(); + } +} diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index b22e16633..8c3b91177 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1849,7 +1849,12 @@ export namespace DocUtils { source: { name, type }, result, } = upfiles.lastElement(); - name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); + if ((result as any).message) { + if (overwriteDoc) { + overwriteDoc.isLoading = false; + overwriteDoc.errorMessage = (result as any).message; + } + } else name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); }); } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 6386c87a0..dfd916e92 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -344,8 +344,7 @@ export namespace DragManager { } Object.assign(dragDiv.style, { width: '', height: '', overflow: '' }); dragDiv.hidden = false; - const scaleXs: number[] = [], - scaleYs: number[] = [], + const scalings: number[] = [], xs: number[] = [], ys: number[] = []; @@ -355,8 +354,15 @@ export namespace DragManager { top: Number.MAX_SAFE_INTEGER, bottom: Number.MIN_SAFE_INTEGER, }; + let rot = 0; const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : []; const dragElements = eles.map(ele => { + if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') { + ele = ele.parentElement.parentElement.parentElement; + const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.]*)deg\).*/, '$1'); + if (rotStr) rot = Number(rotStr); + } + if (rot < 0) rot += 360; if (!ele.parentNode) dragDiv.appendChild(ele); const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement); const children = Array.from(dragElement.children); @@ -376,19 +382,29 @@ export namespace DragManager { } } const rect = ele.getBoundingClientRect(); - const scaleX = rect.width / (ele.offsetWidth || rect.width); - const scaleY = scaleX; //ele.offsetHeight ? rect.height / (ele.offsetHeight || rect.height) : scaleX; + const rotWidth = (rot > 45 && rot < 135) || (rot > 215 && rot < 305) ? rect.height : rect.width; //rect.width * Math.cos((rot * Math.PI) / 180) + rect.height * Math.sin((rot * Math.PI) / 180); + const scaling = rot ? rotWidth / ele.offsetWidth : rect.width / (ele.offsetWidth || rect.width); elesCont.left = Math.min(rect.left, elesCont.left); elesCont.top = Math.min(rect.top, elesCont.top); elesCont.right = Math.max(rect.right, elesCont.right); elesCont.bottom = Math.max(rect.bottom, elesCont.bottom); - xs.push(rect.left + (options?.offsetX || 0)); - ys.push(rect.top + (options?.offsetY || 0)); - scaleXs.push(scaleX); - scaleYs.push(scaleY); + const rotRad = (rot / 180) * Math.PI; + xs.push( + (rot > 90 && rot <= 270 ? rect.right : rect.left) + // + (rot > 270 ? -scaling * (ele.offsetHeight * Math.sin(rotRad)) : 0) + + (rot <= 90 || rot > 180 ? scaling * (ele.offsetHeight * Math.sin(rotRad)) : 0) + + (options?.offsetX || 0) + ); + ys.push( + rect.top + // + (rot > 180 ? -scaling * (ele.offsetWidth * Math.sin(rotRad)) : 0) + + (rot >= 90 && rot < 270 ? -scaling * (ele.offsetHeight * Math.cos(rotRad)) : 0) + + (options?.offsetY || 0) + ); + scalings.push(scaling); Object.assign(dragElement.style, { - opacity: '0.7', + opacity: '0', position: 'absolute', margin: '0', top: '0', @@ -399,9 +415,9 @@ export namespace DragManager { borderRadius: getComputedStyle(ele).borderRadius, zIndex: globalCssVariables.contextMenuZindex, transformOrigin: '0 0', - width: `${rect.width / scaleX}px`, - height: `${rect.height / scaleY}px`, - transform: `translate(${xs[0]}px, ${ys[0]}px) scale(${scaleX}, ${scaleY})`, + width: rot ? '' : `${rect.width / scaling}px`, + height: rot ? '' : `${rect.height / scaling}px`, + transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg)`, }); dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`; @@ -415,6 +431,8 @@ export namespace DragManager { [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => (ele as any).style && ((ele as any).style.pointerEvents = 'none')); dragDiv.appendChild(dragElement); + scalings[scalings.length - 1] = rect.width / dragElement.getBoundingClientRect().width; + setTimeout(() => (dragElement.style.opacity = '0.7')); if (dragElement !== ele) { const children = [Array.from(ele.children), Array.from(dragElement.children)]; while (children[0].length) { @@ -542,7 +560,7 @@ export namespace DragManager { const moveVec = { x: x - lastPt.x, y: y - lastPt.y }; lastPt = { x, y }; - dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`)); + dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot}deg) scale(${scalings[i]})`)); dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`; }; const upHandler = (e: PointerEvent) => { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 01fadb48d..113574a64 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -498,8 +498,8 @@ export class DocumentViewInternal extends DocComponent() { render() { return ( -
+
-

{this.rootDoc.isLoading ? 'Loading:' : 'Error Loading File:'}

+

{this.rootDoc.isLoading ? 'Loading:' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}

{StrCast(this.rootDoc.title)} {!this.rootDoc.isLoading ? null : }
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 0ff15f93b..6ff11258d 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -396,10 +396,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent { - const aspect = this.player!.videoWidth / this.player!.videoHeight; - Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth); - Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight); - this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect; + const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1); + if (aspect) { + Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth); + Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight); + this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect; + } if (Number.isFinite(this.player!.duration)) { this.rawDuration = this.player!.duration; } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index cae35da60..ef7192ecc 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -4,33 +4,33 @@ import * as exifr from 'exifr'; import { File } from 'formidable'; import { createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs'; import * as path from 'path'; -import { basename } from "path"; +import { basename } from 'path'; import * as sharp from 'sharp'; import { Stream } from 'stream'; import { filesDirectory, publicDirectory } from '.'; import { Opt } from '../fields/Doc'; -import { ParsedPDF } from "../server/PdfTypes"; +import { ParsedPDF } from '../server/PdfTypes'; import { Utils } from '../Utils'; import { createIfNotExists } from './ActionUtilities'; import { clientPathToFile, Directory, pathToDirectory, serverPathToFile } from './ApiManagers/UploadManager'; -import { resolvedServerUrl } from "./server_Initialization"; +import { resolvedServerUrl } from './server_Initialization'; import { AcceptableMedia, Upload } from './SharedMediaTypes'; import request = require('request-promise'); import formidable = require('formidable'); import { file } from 'jszip'; import { csvParser } from './DataVizUtils'; -const { exec } = require("child_process"); +const { exec } = require('child_process'); const parse = require('pdf-parse'); -const ffmpeg = require("fluent-ffmpeg"); -const fs = require("fs"); -const requestImageSize = require("../client/util/request-image-size"); +const ffmpeg = require('fluent-ffmpeg'); +const fs = require('fs'); +const requestImageSize = require('../client/util/request-image-size'); export enum SizeSuffix { - Small = "_s", - Medium = "_m", - Large = "_l", - Original = "_o", - None = "" + Small = '_s', + Medium = '_m', + Large = '_l', + Original = '_o', + None = '', } export function InjectSize(filename: string, size: SizeSuffix) { @@ -43,7 +43,6 @@ function isLocal() { } export namespace DashUploadUtils { - export interface Size { width: number; suffix: SizeSuffix; @@ -59,19 +58,19 @@ export namespace DashUploadUtils { return AcceptableMedia.imageFormats.includes(path.extname(url).toLowerCase()); } - const size = "content-length"; - const type = "content-type"; + const size = 'content-length'; + const type = 'content-type'; + + const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr - const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr - export async function concatVideos(filePaths: string[]): Promise { // make a list of paths to create the ordered text file for ffmpeg const inputListName = 'concat.txt'; const textFilePath = path.join(filesDirectory, inputListName); // make a list of paths to create the ordered text file for ffmpeg - const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n'); + const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n'); // write the text file to the file system - writeFile(textFilePath, filePathsText, (err) => console.log(err)); + writeFile(textFilePath, filePathsText, err => console.log(err)); // make output file name based on timestamp const outputFileName = `output-${Utils.GenerateGuid()}.mp4`; @@ -81,87 +80,110 @@ export namespace DashUploadUtils { // concatenate the videos await new Promise((resolve, reject) => { var merge = ffmpeg(); - merge.input(textFilePath) - .inputOptions(['-f concat', '-safe 0']) + merge + .input(textFilePath) + .inputOptions(['-f concat', '-safe 0']) .outputOptions('-c copy') //.videoCodec("copy") .save(outputFilePath) - .on("error", reject) - .on("end", resolve); - }) - - // delete concat.txt from the file system - unlinkSync(textFilePath); - // delete the old segment videos from the server - filePaths.forEach(filePath => unlinkSync(filePath)); - - // return the path(s) to the output file - return { - accessPaths: getAccessPaths(Directory.videos, outputFileName) - } + .on('error', reject) + .on('end', resolve); + }); + + // delete concat.txt from the file system + unlinkSync(textFilePath); + // delete the old segment videos from the server + filePaths.forEach(filePath => unlinkSync(filePath)); + + // return the path(s) to the output file + return { + accessPaths: getAccessPaths(Directory.videos, outputFileName), + }; } export function uploadYoutube(videoId: string): Promise { - console.log("UPLOAD " + videoId); + console.log('UPLOAD ' + videoId); return new Promise>((res, rej) => { - exec('youtube-dl -o ' + (videoId + ".mp4") + ' https://www.youtube.com/watch?v=' + videoId + ' -f "best[filesize<50M]"', - (error: any, stdout: any, stderr: any) => { - if (error) console.log(`error: ${error.message}`); - else if (stderr) console.log(`stderr: ${stderr}`); - else { - console.log(`stdout: ${stdout}`); - const data = { size: 0, path: videoId + ".mp4", name: videoId, type: "video/mp4" }; - const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ""), mtime: null, length: 0, mime: "", toJson: () => undefined as any }) }; - res(MoveParsedFile(file, Directory.videos)); - } - }); + exec('youtube-dl -o ' + (videoId + '.mp4') + ' https://www.youtube.com/watch?v=' + videoId + ' -f "best[filesize<50M]"', (error: any, stdout: any, stderr: any) => { + if (error) console.log(`error: ${error.message}`); + else if (stderr) console.log(`stderr: ${stderr}`); + else { + console.log(`stdout: ${stdout}`); + const data = { size: 0, path: videoId + '.mp4', name: videoId, type: 'video/mp4' }; + const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ''), mtime: null, length: 0, mime: '', toJson: () => undefined as any }) }; + res(MoveParsedFile(file, Directory.videos)); + } + }); }); } export async function upload(file: File): Promise { const { type, path, name } = file; - const types = type?.split("/") ?? []; + const types = type?.split('/') ?? []; const category = types[0]; let format = `.${types[1]}`; console.log(green(`Processing upload of file (${name}) and format (${format}) with upload type (${type}) in category (${category}).`)); - + switch (category) { - case "image": + case 'image': if (imageFormats.includes(format)) { const result = await UploadImage(path, basename(path)); return { source: file, result }; } - case "video": - if (format.includes("x-matroska")) { - console.log("case video"); - await new Promise(res => ffmpeg(file.path) - .videoCodec("copy") // this will copy the data instead of reencode it - .save(file.path.replace(".mkv", ".mp4")) - .on('end', res)); - file.path = file.path.replace(".mkv", ".mp4"); - format = ".mp4"; + case 'video': + if (format.includes('x-matroska')) { + console.log('case video'); + await new Promise(res => + ffmpeg(file.path) + .videoCodec('copy') // this will copy the data instead of reencode it + .save(file.path.replace('.mkv', '.mp4')) + .on('end', res) + ); + file.path = file.path.replace('.mkv', '.mp4'); + format = '.mp4'; + } + if (format.includes('quicktime')) { + let abort = false; + await new Promise(res => + ffmpeg.ffprobe(file.path, (err: any, metadata: any) => { + if (metadata.streams.some((stream: any) => stream.codec_name === 'hevc')) { + abort = true; + } + res(); + }) + ); + if (abort) return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${name}). Please convert to an .mp4` } }; + // bcz: instead of aborting, we could convert the file using the code below to an mp4. Problem is that this takes a long time and will clog up the server. + // await new Promise(res => + // ffmpeg(file.path) + // .videoCodec('libx264') // this will copy the data instead of reencode it + // .audioCodec('mp2') + // .save(file.path.replace('.MOV', '.mp4').replace('.mov', '.mp4')) + // .on('end', res) + // ); + // file.path = file.path.replace('.mov', '.mp4').replace('.MOV', '.mp4'); + // format = '.mp4'; } if (videoFormats.includes(format)) { return MoveParsedFile(file, Directory.videos); } - case "application": + case 'application': if (applicationFormats.includes(format)) { return UploadPdf(file); } - case "audio": - const components = format.split(";"); + case 'audio': + const components = format.split(';'); if (components.length > 1) { format = components[0]; } if (audioFormats.includes(format)) { return UploadAudio(file, format); } - case "text": - if (types[1] == "csv") { + case 'text': + if (types[1] == 'csv') { return UploadCsv(file); } - } console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`)); @@ -176,22 +198,21 @@ export namespace DashUploadUtils { const name = path.basename(sourcePath); const textFilename = `${name.substring(0, name.length - 4)}.txt`; const writeStream = createWriteStream(serverPathToFile(Directory.text, textFilename)); - writeStream.write(result.text, error => error ? reject(error) : resolve()); + writeStream.write(result.text, error => (error ? reject(error) : resolve())); }); return MoveParsedFile(file, Directory.pdfs, undefined, result.text); } async function UploadCsv(file: File) { - const { path: sourcePath } = file; - // read the file as a string + const { path: sourcePath } = file; + // read the file as a string const data = readFileSync(sourcePath, 'utf8'); // split the string into an array of lines return MoveParsedFile(file, Directory.csv, undefined, data); // console.log(csvParser(data)); - } - const manualSuffixes = [".webm"]; + const manualSuffixes = ['.webm']; async function UploadAudio(file: File, format: string) { const suffix = manualSuffixes.includes(format) ? format : undefined; @@ -200,22 +221,22 @@ export namespace DashUploadUtils { /** * Uploads an image specified by the @param source to Dash's /public/files/ - * directory, and returns information generated during that upload - * + * directory, and returns information generated during that upload + * * @param {string} source is either the absolute path of an already uploaded image or * the url of a remote image * @param {string} filename dictates what to call the image. If not specified, * the name {@param prefix}_upload_{GUID} * @param {string} prefix is a string prepended to the generated image name in the * event that @param filename is not specified - * + * * @returns {ImageUploadInformation | Error} This method returns * 1) the paths to the uploaded images (plural due to resizing) * 2) the exif data embedded in the image, or the error explaining why exif couldn't be parsed * 3) the size of the image, in bytes (4432130) * 4) the content type of the image, i.e. image/(jpeg | png | ...) */ - export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise => { + export const UploadImage = async (source: string, filename?: string, prefix: string = ''): Promise => { const metadata = await InspectImage(source); if (metadata instanceof Error) { return metadata; @@ -225,12 +246,12 @@ export namespace DashUploadUtils { export async function buildFileDirectories() { if (!existsSync(publicDirectory)) { - console.error("\nPlease ensure that the following directory exists...\n"); + console.error('\nPlease ensure that the following directory exists...\n'); console.log(publicDirectory); process.exit(0); } if (!existsSync(filesDirectory)) { - console.error("\nPlease ensure that the following directory exists...\n"); + console.error('\nPlease ensure that the following directory exists...\n'); console.log(filesDirectory); process.exit(0); } @@ -252,7 +273,7 @@ export namespace DashUploadUtils { /** * Based on the url's classification as local or remote, gleans * as much information as possible about the specified image - * + * * @param source is the path or url to the image in question */ export const InspectImage = async (source: string): Promise => { @@ -265,9 +286,9 @@ export namespace DashUploadUtils { */ if ((rawMatches = /^data:image\/([a-z]+);base64,(.*)/.exec(source)) !== null) { const [ext, data] = rawMatches.slice(1, 3); - const resolved = filename = `upload_${Utils.GenerateGuid()}.${ext}`; + const resolved = (filename = `upload_${Utils.GenerateGuid()}.${ext}`); const error = await new Promise(resolve => { - writeFile(serverPathToFile(Directory.images, resolved), data, "base64", resolve); + writeFile(serverPathToFile(Directory.images, resolved), data, 'base64', resolve); }); if (error !== null) { return error; @@ -276,12 +297,12 @@ export namespace DashUploadUtils { } let resolvedUrl: string; /** - * + * * At this point, we want to take whatever url we have and make sure it's requestable. * Anything that's hosted by some other website already is, but if the url is a local file url * (locates the file on this server machine), we have to resolve the client side url by cutting out the * basename subtree (i.e. /images/.) and put it on the end of the server's url. - * + * * This can always be localhost, regardless of whether this is on the server or not, since we (the server, not the client) * will be the ones making the request, and from the perspective of dash-release or dash-web, localhost: refers to the same thing * as the full dash-release.eastus.cloudapp.azure.com:. @@ -290,18 +311,18 @@ export namespace DashUploadUtils { if (matches === null) { resolvedUrl = source; } else { - resolvedUrl = `${resolvedServerUrl}/${matches[1].split("\\").join("/")}`; + resolvedUrl = `${resolvedServerUrl}/${matches[1].split('\\').join('/')}`; } // See header comments: not all image files have exif data (I believe only JPG is the only format that can have it) const exifData = await parseExifData(resolvedUrl); const results = { exifData, - requestable: resolvedUrl + requestable: resolvedUrl, }; // Use the request library to parse out file level image information in the headers - const { headers } = (await new Promise((resolve, reject) => { - request.head(resolvedUrl, (error, res) => error ? reject(error) : resolve(res)); - }).catch(console.error)); + const { headers } = await new Promise((resolve, reject) => { + request.head(resolvedUrl, (error, res) => (error ? reject(error) : resolve(res))); + }).catch(console.error); try { // Compute the native width and height ofthe image with an npm module const { width: nativeWidth, height: nativeHeight } = await requestImageSize(resolvedUrl); @@ -313,7 +334,7 @@ export namespace DashUploadUtils { nativeWidth, nativeHeight, filename, - ...results + ...results, }; } catch (e: any) { console.log(e); @@ -340,12 +361,14 @@ export namespace DashUploadUtils { rename(sourcePath, destinationPath, error => { resolve({ source: file, - result: error ? error : { - accessPaths: { - agnostic: getAccessPaths(destination, name) - }, - rawText: text - } + result: error + ? error + : { + accessPaths: { + agnostic: getAccessPaths(destination, name), + }, + rawText: text, + }, }); }); }); @@ -354,19 +377,19 @@ export namespace DashUploadUtils { export function getAccessPaths(directory: Directory, fileName: string) { return { client: clientPathToFile(directory, fileName), - server: serverPathToFile(directory, fileName) + server: serverPathToFile(directory, fileName), }; } - export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = "", cleanUp = true): Promise => { + export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = '', cleanUp = true): Promise => { const { requestable, source, ...remaining } = metadata; - const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split("/")[1].toLowerCase()}`; + const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split('/')[1].toLowerCase()}`; const { images } = Directory; const information: Upload.ImageInformation = { accessPaths: { - agnostic: getAccessPaths(images, resolved) + agnostic: getAccessPaths(images, resolved), }, - ...metadata + ...metadata, }; const writtenFiles = await outputResizedImages(() => request(requestable), resolved, pathToDirectory(Directory.images)); for (const suffix of Object.keys(writtenFiles)) { @@ -383,9 +406,9 @@ export namespace DashUploadUtils { const val: any = layer[key]; if (val instanceof Buffer) { layer[key] = val.toString(); - } else if (Array.isArray(val) && typeof val[0] === "number") { + } else if (Array.isArray(val) && typeof val[0] === 'number') { layer[key] = Buffer.from(val).toString(); - } else if (typeof val === "object") { + } else if (typeof val === 'object') { bufferConverterRec(val); } } @@ -410,20 +433,20 @@ export namespace DashUploadUtils { const pngOptions = { compressionLevel: 9, adaptiveFiltering: true, - force: true + force: true, }; export async function outputResizedImages(streamProvider: () => Stream | Promise, outputFileName: string, outputDirectory: string) { const writtenFiles: { [suffix: string]: string } = {}; for (const { resizer, suffix } of resizers(path.extname(outputFileName))) { - const outputPath = path.resolve(outputDirectory, writtenFiles[suffix] = InjectSize(outputFileName, suffix)); + const outputPath = path.resolve(outputDirectory, (writtenFiles[suffix] = InjectSize(outputFileName, suffix))); await new Promise(async (resolve, reject) => { const source = streamProvider(); let readStream: Stream = source instanceof Promise ? await source : source; if (resizer) { readStream = readStream.pipe(resizer.withMetadata()); } - readStream.pipe(createWriteStream(outputPath)).on("close", resolve).on("error", reject); + readStream.pipe(createWriteStream(outputPath)).on('close', resolve).on('error', reject); }); } return writtenFiles; @@ -442,15 +465,14 @@ export namespace DashUploadUtils { initial = initial.webp(); } else if (tiffs.includes(ext)) { initial = initial.tiff(); - } else if (ext === ".gif") { + } else if (ext === '.gif') { initial = undefined; } return { resizer: initial, - suffix + suffix, }; - }) + }), ]; } - -} \ No newline at end of file +} -- cgit v1.2.3-70-g09d2 From e70360946815cdcde434e25eb592e1b919bb4105 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 6 Sep 2022 14:06:02 -0400 Subject: final cleanup of dragging rotated images. fixed loading of youtube videos and displaying errors when the upload fails --- src/client/documents/Documents.ts | 13 +- src/client/util/DragManager.ts | 39 ++-- src/client/views/PreviewCursor.tsx | 55 ++++-- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- .../collections/collectionFreeForm/MarqueeView.tsx | 12 +- src/client/views/nodes/LoadingBox.tsx | 10 +- src/client/views/nodes/VideoBox.scss | 20 +- src/client/views/nodes/VideoBox.tsx | 212 ++++++++++----------- src/server/ApiManagers/UploadManager.ts | 2 +- src/server/DashUploadUtils.ts | 48 +++-- src/server/SharedMediaTypes.ts | 38 ++-- 11 files changed, 257 insertions(+), 194 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 8c3b91177..7111cb233 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,5 +1,4 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { files } from 'jszip'; import { action, runInAction } from 'mobx'; import { basename } from 'path'; import { DateField } from '../../fields/DateField'; @@ -1780,6 +1779,9 @@ export namespace DocUtils { proto.lng = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection); } } + if (Upload.isVideoInformation(result)) { + proto['data-duration'] = result.duration; + } generatedDocuments.push(doc); } } @@ -1814,7 +1816,7 @@ export namespace DocUtils { source: { name, type }, result, } of await Networking.UploadYoutubeToServer(videoId)) { - name && type && processFileupload(generatedDocuments, name, type, result, options); + name && processFileupload(generatedDocuments, name, type, result, options); } return generatedDocuments; } @@ -1826,7 +1828,12 @@ export namespace DocUtils { source: { name, type }, result, } = upfiles.lastElement(); - name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); + if ((result as any).message) { + if (overwriteDoc) { + overwriteDoc.isLoading = false; + overwriteDoc.errorMessage = (result as any).message; + } + } else name && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc); }); } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index dfd916e92..cec158d23 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -382,29 +382,28 @@ export namespace DragManager { } } const rect = ele.getBoundingClientRect(); - const rotWidth = (rot > 45 && rot < 135) || (rot > 215 && rot < 305) ? rect.height : rect.width; //rect.width * Math.cos((rot * Math.PI) / 180) + rect.height * Math.sin((rot * Math.PI) / 180); - const scaling = rot ? rotWidth / ele.offsetWidth : rect.width / (ele.offsetWidth || rect.width); + const w = ele.offsetWidth || rect.width; + const h = ele.offsetHeight || rect.height; + const rotR = -(rot / 180) * Math.PI; + const tl = [0, 0]; + const tr = [Math.cos(rotR) * w, Math.sin(-rotR) * w]; + const bl = [Math.sin(rotR) * h, Math.cos(-rotR) * h]; + const br = [Math.cos(rotR) * w + Math.sin(rotR) * h, Math.cos(-rotR) * h - Math.sin(rotR) * w]; + const minx = Math.min(tl[0], tr[0], br[0], bl[0]); + const maxx = Math.max(tl[0], tr[0], br[0], bl[0]); + const miny = Math.min(tl[1], tr[1], br[1], bl[1]); + const maxy = Math.max(tl[1], tr[1], br[1], bl[1]); + const scaling = rect.width / (Math.abs(maxx - minx) || 1); elesCont.left = Math.min(rect.left, elesCont.left); elesCont.top = Math.min(rect.top, elesCont.top); elesCont.right = Math.max(rect.right, elesCont.right); elesCont.bottom = Math.max(rect.bottom, elesCont.bottom); - const rotRad = (rot / 180) * Math.PI; - xs.push( - (rot > 90 && rot <= 270 ? rect.right : rect.left) + // - (rot > 270 ? -scaling * (ele.offsetHeight * Math.sin(rotRad)) : 0) + - (rot <= 90 || rot > 180 ? scaling * (ele.offsetHeight * Math.sin(rotRad)) : 0) + - (options?.offsetX || 0) - ); - ys.push( - rect.top + // - (rot > 180 ? -scaling * (ele.offsetWidth * Math.sin(rotRad)) : 0) + - (rot >= 90 && rot < 270 ? -scaling * (ele.offsetHeight * Math.cos(rotRad)) : 0) + - (options?.offsetY || 0) - ); + xs.push(((0 - minx) / (maxx - minx)) * rect.width + rect.left); + ys.push(((0 - miny) / (maxy - miny)) * rect.height + rect.top); scalings.push(scaling); Object.assign(dragElement.style, { - opacity: '0', + opacity: '0.7', position: 'absolute', margin: '0', top: '0', @@ -415,9 +414,9 @@ export namespace DragManager { borderRadius: getComputedStyle(ele).borderRadius, zIndex: globalCssVariables.contextMenuZindex, transformOrigin: '0 0', - width: rot ? '' : `${rect.width / scaling}px`, - height: rot ? '' : `${rect.height / scaling}px`, - transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg)`, + width: '', + height: '', + transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`, }); dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`; @@ -431,8 +430,6 @@ export namespace DragManager { [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => (ele as any).style && ((ele as any).style.pointerEvents = 'none')); dragDiv.appendChild(dragElement); - scalings[scalings.length - 1] = rect.width / dragElement.getBoundingClientRect().width; - setTimeout(() => (dragElement.style.opacity = '0.7')); if (dragElement !== ele) { const children = [Array.from(ele.children), Array.from(dragElement.children)]; while (children[0].length) { diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 68f5f072d..4c17d5a97 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -4,9 +4,9 @@ import 'normalize.css'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; import { Cast, NumCast, StrCast } from '../../fields/Types'; -import { returnFalse } from '../../Utils'; +import { emptyFunction, returnFalse } from '../../Utils'; import { DocServer } from '../DocServer'; -import { Docs, DocUtils } from '../documents/Documents'; +import { Docs, DocumentOptions, DocUtils } from '../documents/Documents'; import { Transform } from '../util/Transform'; import { undoBatch, UndoManager } from '../util/UndoManager'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; @@ -16,15 +16,25 @@ import './PreviewCursor.scss'; export class PreviewCursor extends React.Component<{}> { static _onKeyPress?: (e: KeyboardEvent) => void; static _getTransform: () => Transform; - static _addDocument: (doc: Doc | Doc[]) => void; + static _addDocument: (doc: Doc | Doc[]) => boolean; static _addLiveTextDoc: (doc: Doc) => void; static _nudge?: undefined | ((x: number, y: number) => boolean); + static _slowLoadDocuments?: ( + files: File[] | string, + options: DocumentOptions, + generatedDocuments: Doc[], + text: string, + completed: ((doc: Doc[]) => void) | undefined, + clientX: number, + clientY: number, + addDocument: (doc: Doc | Doc[]) => boolean + ) => Promise; @observable static _clickPoint = [0, 0]; @observable public static Visible = false; constructor(props: any) { super(props); document.addEventListener('keydown', this.onKeyPress); - document.addEventListener('paste', this.paste); + document.addEventListener('paste', this.paste, true); } paste = async (e: ClipboardEvent) => { @@ -38,20 +48,16 @@ export class PreviewCursor extends React.Component<{}> { if (plain) { // tests for youtube and makes video document if (plain.indexOf('www.youtube.com/watch') !== -1) { - const url = plain.replace('youtube.com/watch?v=', 'youtube.com/embed/'); - undoBatch(() => - PreviewCursor._addDocument( - Docs.Create.VideoDocument(url, { - title: url, - _width: 400, - _height: 315, - _nativeWidth: 600, - _nativeHeight: 472.5, - x: newPoint[0], - y: newPoint[1], - }) - ) - )(); + const batch = UndoManager.StartBatch('youtube upload'); + const generatedDocuments: Doc[] = []; + const options = { + title: plain, + _width: 400, + _height: 315, + x: newPoint[0], + y: newPoint[1], + }; + PreviewCursor._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, newPoint[0], newPoint[1], PreviewCursor._addDocument).then(batch.end); } else if (re.test(plain)) { const url = plain; undoBatch(() => @@ -184,7 +190,17 @@ export class PreviewCursor extends React.Component<{}> { addLiveText: (doc: Doc) => void, getTransform: () => Transform, addDocument: undefined | ((doc: Doc | Doc[]) => boolean), - nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean) + nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean), + slowLoadDocuments: ( + files: File[] | string, + options: DocumentOptions, + generatedDocuments: Doc[], + text: string, + completed: ((doc: Doc[]) => void) | undefined, + clientX: number, + clientY: number, + addDocument: (doc: Doc | Doc[]) => boolean + ) => Promise ) { this._clickPoint = [x, y]; this._onKeyPress = onKeyPress; @@ -192,6 +208,7 @@ export class PreviewCursor extends React.Component<{}> { this._getTransform = getTransform; this._addDocument = addDocument || returnFalse; this._nudge = nudge; + this._slowLoadDocuments = slowLoadDocuments; this.Visible = true; } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 0c4de681a..947bd8aaa 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -52,7 +52,6 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; import React = require('react'); -import e = require('connect-flash'); export type collectionFreeformViewProps = { annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox) @@ -1858,6 +1857,7 @@ export class CollectionFreeFormView extends CollectionSubView 0 ? undefined : this.nudge} addDocTab={this.addDocTab} + slowLoadDocuments={this.slowLoadDocuments} trySelectCluster={this.trySelectCluster} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 65a11cbcb..58a00bbac 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -40,6 +40,16 @@ interface MarqueeViewProps { nudge?: (x: number, y: number, nudgeTime?: number) => boolean; ungroup?: () => void; setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; + slowLoadDocuments: ( + files: File[] | string, + options: DocumentOptions, + generatedDocuments: Doc[], + text: string, + completed: ((doc: Doc[]) => void) | undefined, + clientX: number, + clientY: number, + addDocument: (doc: Doc | Doc[]) => boolean + ) => Promise; } export interface MarqueeViewBounds { @@ -330,7 +340,7 @@ export class MarqueeView extends React.Component() { return (
-

{this.rootDoc.isLoading ? 'Loading:' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}

+

{this.rootDoc.isLoading ? 'Loading (can take several minutes):' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}

{StrCast(this.rootDoc.title)} {!this.rootDoc.isLoading ? null : }
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index aa51714da..c2aee7a1b 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,4 +1,4 @@ -@import "../global/globalCssVariables.scss"; +@import '../global/globalCssVariables.scss'; .mini-viewer { cursor: grab; @@ -97,7 +97,7 @@ height: 40px; padding: 0 10px 0 7px; transition: opacity 0.3s; - z-index: 100001; + z-index: 10001; .timecode-controls { display: flex; @@ -114,7 +114,8 @@ } } - .toolbar-slider.volume, .toolbar-slider.zoom { + .toolbar-slider.volume, + .toolbar-slider.zoom { width: 50px; } @@ -157,7 +158,8 @@ } } -.videoBox-content-fullScreen, .videoBox-content-fullScreen-interactive { +.videoBox-content-fullScreen, +.videoBox-content-fullScreen-interactive { display: flex; justify-content: center; align-items: flex-end; @@ -175,16 +177,16 @@ video::-webkit-media-controls { display: none !important; } -input[type="range"] { +input[type='range'] { -webkit-appearance: none; background: none; } -input[type="range"]:focus { +input[type='range']:focus { outline: none; } -input[type="range"]::-webkit-slider-runnable-track { +input[type='range']::-webkit-slider-runnable-track { width: 100%; height: 10px; cursor: pointer; @@ -193,7 +195,7 @@ input[type="range"]::-webkit-slider-runnable-track { border-radius: 10px; } -input[type="range"]::-webkit-slider-thumb { +input[type='range']::-webkit-slider-thumb { box-shadow: 0; border: 0; height: 12px; @@ -203,4 +205,4 @@ input[type="range"]::-webkit-slider-thumb { cursor: pointer; -webkit-appearance: none; margin-top: -1px; -} \ No newline at end of file +} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 6ff11258d..bfb8c1528 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -935,112 +935,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent -
- -
- - {this.timeline && width > 150 && ( -
-
{formatTime(curTime)}
- - {this._fullScreen || (this.heightPercent === 100 && width > 200) ? ( -
- { - e.stopPropagation(); - this._scrubbing = true; - })} - onChange={(e: React.ChangeEvent) => this.setPlayheadTime(Number(e.target.value))} - onPointerUp={action((e: React.PointerEvent) => { - e.stopPropagation(); - this._scrubbing = false; - })} - /> -
- ) : ( -
/
- )} - -
{formatTime(this.timeline.clipDuration)}
-
- )} - -
- -
- - {!this._fullScreen && width > 300 && ( -
- -
- )} - - {!this._fullScreen && width > 300 && ( -
- -
- )} - -
{ - e.stopPropagation(); - this.toggleMute(); - }}> - -
- {width > 300 && ( - e.stopPropagation()} - onChange={(e: React.ChangeEvent) => this.setVolume(Number(e.target.value))} - /> - )} - - {!this._fullScreen && this.heightPercent !== 100 && width > 300 && ( - <> -
- -
- { - e.stopPropagation(); - }} - onChange={(e: React.ChangeEvent) => { - this.zoom(Number(e.target.value)); - }} - /> - - )} - - ); - } - // renders CollectionStackedTimeline @computed get renderTimeline() { return ( @@ -1149,6 +1043,112 @@ export class VideoBox extends ViewBoxAnnotatableComponent ); } + + @computed get UIButtons() { + const bounds = this.props.docViewPath().lastElement().getBounds(); + const width = (bounds?.right || 0) - (bounds?.left || 0); + const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0); + return ( + <> +
+ +
+ + {this.timeline && width > 150 && ( +
+
{formatTime(curTime)}
+ + {this._fullScreen || (this.heightPercent === 100 && width > 200) ? ( +
+ { + e.stopPropagation(); + this._scrubbing = true; + })} + onChange={(e: React.ChangeEvent) => this.setPlayheadTime(Number(e.target.value))} + onPointerUp={action((e: React.PointerEvent) => { + e.stopPropagation(); + this._scrubbing = false; + })} + /> +
+ ) : ( +
/
+ )} + +
{formatTime(this.timeline.clipDuration)}
+
+ )} + +
+ +
+ + {!this._fullScreen && width > 300 && ( +
+ +
+ )} + + {!this._fullScreen && width > 300 && ( +
+ +
+ )} + +
{ + e.stopPropagation(); + this.toggleMute(); + }}> + +
+ {width > 300 && ( + e.stopPropagation()} + onChange={(e: React.ChangeEvent) => this.setVolume(Number(e.target.value))} + /> + )} + + {!this._fullScreen && this.heightPercent !== 100 && width > 300 && ( + <> +
+ +
+ { + e.stopPropagation(); + }} + onChange={(e: React.ChangeEvent) => { + this.zoom(Number(e.target.value)); + }} + /> + + )} + + ); + } } VideoBox._nativeControls = false; diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 787e331c5..0b6e18743 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -86,7 +86,7 @@ export default class UploadManager extends ApiManager { const videoId = JSON.parse(payload).videoId; const results: Upload.FileResponse[] = []; const result = await DashUploadUtils.uploadYoutube(videoId); - result && !(result.result instanceof Error) && results.push(result); + result && results.push(result); _success(res, results); resolve(); }); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index ef7192ecc..28e26e51e 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -17,8 +17,6 @@ import { resolvedServerUrl } from './server_Initialization'; import { AcceptableMedia, Upload } from './SharedMediaTypes'; import request = require('request-promise'); import formidable = require('formidable'); -import { file } from 'jszip'; -import { csvParser } from './DataVizUtils'; const { exec } = require('child_process'); const parse = require('pdf-parse'); const ffmpeg = require('fluent-ffmpeg'); @@ -102,16 +100,41 @@ export namespace DashUploadUtils { } export function uploadYoutube(videoId: string): Promise { - console.log('UPLOAD ' + videoId); return new Promise>((res, rej) => { - exec('youtube-dl -o ' + (videoId + '.mp4') + ' https://www.youtube.com/watch?v=' + videoId + ' -f "best[filesize<50M]"', (error: any, stdout: any, stderr: any) => { - if (error) console.log(`error: ${error.message}`); - else if (stderr) console.log(`stderr: ${stderr}`); - else { - console.log(`stdout: ${stdout}`); - const data = { size: 0, path: videoId + '.mp4', name: videoId, type: 'video/mp4' }; - const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ''), mtime: null, length: 0, mime: '', toJson: () => undefined as any }) }; - res(MoveParsedFile(file, Directory.videos)); + console.log('Uploading YouTube video: ' + videoId); + exec('youtube-dl -o ' + (videoId + '.mp4') + ' ' + videoId + ' -f "bestvideo[filesize<5M]+bestaudio/bestvideo+bestaudio"', (error: any, stdout: any, stderr: any) => { + if (error) { + console.log(`error: Error: ${error.message}`); + res({ + source: { + size: 0, + path: videoId, + name: videoId, + type: '', + toJSON: () => ({ name: videoId, path: videoId }), + }, + result: { name: 'failed youtube query', message: `Could not upload YouTube video (${videoId}). Error: ${error.message}` }, + }); + } else if (stderr) { + console.log(`stderr: StdError: ${stderr}`); + res({ + source: { + size: 0, + path: videoId, + name: videoId, + type: '', + toJSON: () => ({ name: videoId, path: videoId }), + }, + result: { name: 'failed youtube query', message: `Could not upload YouTube video (${videoId}). Error: ${stderr}` }, + }); + } else { + exec('youtube-dl -o ' + (videoId + '.mp4') + ' ' + videoId + ' --get-duration', (error: any, stdout: any, stderr: any) => { + const time = Array.from(stdout.trim().split(':')).reverse(); + const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0); + const data = { size: 0, path: videoId + '.mp4', name: videoId, type: 'video/mp4' }; + const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ''), mtime: duration.toString(), mime: '', toJson: () => undefined as any }) }; + res(MoveParsedFile(file, Directory.videos)); + }); } }); }); @@ -352,7 +375,7 @@ export namespace DashUploadUtils { * @param suffix If the file doesn't have a suffix and you want to provide it one * to appear in the new location */ - export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string): Promise { + export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string, duration?: number): Promise { const { path: sourcePath } = file; let name = path.basename(sourcePath); suffix && (name += suffix); @@ -368,6 +391,7 @@ export namespace DashUploadUtils { agnostic: getAccessPaths(destination, name), }, rawText: text, + duration, }, }); }); diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts index cde95526f..7db1c2dae 100644 --- a/src/server/SharedMediaTypes.ts +++ b/src/server/SharedMediaTypes.ts @@ -2,36 +2,45 @@ import { ExifData } from 'exif'; import { File } from 'formidable'; export namespace AcceptableMedia { - export const gifs = [".gif"]; - export const pngs = [".png"]; - export const jpgs = [".jpg", ".jpeg"]; - export const webps = [".webp"]; - export const tiffs = [".tiff"]; + export const gifs = ['.gif']; + export const pngs = ['.png']; + export const jpgs = ['.jpg', '.jpeg']; + export const webps = ['.webp']; + export const tiffs = ['.tiff']; export const imageFormats = [...pngs, ...jpgs, ...gifs, ...webps, ...tiffs]; - export const videoFormats = [".mov", ".mp4", ".quicktime", ".mkv", ".x-matroska;codecs=avc1"]; - export const applicationFormats = [".pdf"]; - export const audioFormats = [".wav", ".mp3", ".mpeg", ".flac", ".au", ".aiff", ".m4a", ".webm"]; + export const videoFormats = ['.mov', '.mp4', '.quicktime', '.mkv', '.x-matroska;codecs=avc1']; + export const applicationFormats = ['.pdf']; + export const audioFormats = ['.wav', '.mp3', '.mpeg', '.flac', '.au', '.aiff', '.m4a', '.webm']; } export namespace Upload { - export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation { - return "nativeWidth" in uploadResponse; + return 'nativeWidth' in uploadResponse; + } + + export function isVideoInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.VideoInformation { + return 'duration' in uploadResponse; } export interface FileInformation { accessPaths: AccessPathInfo; rawText?: string; + duration?: number; } - export type FileResponse = { source: File, result: T | Error }; + export type FileResponse = { source: File; result: T | Error }; export type ImageInformation = FileInformation & InspectionResults; + export type VideoInformation = FileInformation & VideoResults; + export interface AccessPathInfo { - [suffix: string]: { client: string, server: string }; + [suffix: string]: { client: string; server: string }; } + export interface VideoResults { + duration: number; + } export interface InspectionResults { source: string; requestable: string; @@ -44,8 +53,7 @@ export namespace Upload { } export interface EnrichedExifData { - data: ExifData & ExifData["gps"]; + data: ExifData & ExifData['gps']; error?: string; } - -} \ No newline at end of file +} -- cgit v1.2.3-70-g09d2 From 2d8f763d763080fd52e940abb1a98b41e2da23fd Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 7 Sep 2022 14:28:44 -0400 Subject: fixed dragging stackedTimeline entries so that they appear during drag. fixed cursors for video/pdf/stackedTimeline to be a little more clear. fixed initial corner resize of pdfs. fixed videobox marquee drag when viewScale is undefined. --- src/client/util/DragManager.ts | 9 +++++++-- src/client/views/DocumentDecorations.tsx | 3 +++ src/client/views/MarqueeAnnotator.scss | 9 ++++----- src/client/views/MarqueeAnnotator.tsx | 3 +-- .../views/collections/CollectionStackedTimeline.scss | 4 +++- .../views/collections/CollectionStackedTimeline.tsx | 4 +++- .../collections/collectionFreeForm/MarqueeView.scss | 20 +++++++++----------- src/client/views/nodes/VideoBox.tsx | 4 ++-- 8 files changed, 32 insertions(+), 24 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index cec158d23..664933de0 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -357,10 +357,13 @@ export namespace DragManager { let rot = 0; const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : []; const dragElements = eles.map(ele => { + let useDim = false; if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') { ele = ele.parentElement.parentElement.parentElement; const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.]*)deg\).*/, '$1'); if (rotStr) rot = Number(rotStr); + } else { + useDim = true; } if (rot < 0) rot += 360; if (!ele.parentNode) dragDiv.appendChild(ele); @@ -402,6 +405,8 @@ export namespace DragManager { xs.push(((0 - minx) / (maxx - minx)) * rect.width + rect.left); ys.push(((0 - miny) / (maxy - miny)) * rect.height + rect.top); scalings.push(scaling); + const width = useDim ? getComputedStyle(ele).width : ''; + const height = useDim ? getComputedStyle(ele).height : ''; Object.assign(dragElement.style, { opacity: '0.7', position: 'absolute', @@ -414,8 +419,8 @@ export namespace DragManager { borderRadius: getComputedStyle(ele).borderRadius, zIndex: globalCssVariables.contextMenuZindex, transformOrigin: '0 0', - width: '', - height: '', + width, + height, transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`, }); dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 3589e014a..a79f727a7 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -476,6 +476,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions(); if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { const doc = Document(docView.rootDoc); + if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight)) { + doc._nativeHeight = (NumCast(doc._height) / NumCast(doc._width, 1)) * docView.nativeWidth; + } const nwidth = docView.nativeWidth; const nheight = docView.nativeHeight; let docheight = doc._height || 0; diff --git a/src/client/views/MarqueeAnnotator.scss b/src/client/views/MarqueeAnnotator.scss index c90d48a65..5c65f35e9 100644 --- a/src/client/views/MarqueeAnnotator.scss +++ b/src/client/views/MarqueeAnnotator.scss @@ -1,12 +1,11 @@ - .marqueeAnnotator-annotationBox { position: absolute; background-color: rgba(245, 230, 95, 0.616); } - .marqueeAnnotator-dragBox { - position:absolute; + position: absolute; background-color: transparent; - opacity: 0.1; -} \ No newline at end of file + opacity: 0.2; + cursor: default; +} diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index f90ad8bb5..d9a989309 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -271,8 +271,7 @@ export class MarqueeAnnotator extends React.Component { width: `${this._width}px`, height: `${this._height}px`, border: `${this._width === 0 ? '' : '2px dashed black'}`, - opacity: 0.2, - }}>
+ }}/> ); } } diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index aa8502c20..5a107d2ca 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -10,6 +10,7 @@ border-width: 0 2px 0 2px; &:hover { + cursor: default; .collectionStackedTimeline-hover { display: block; } @@ -109,14 +110,15 @@ height: 100%; width: 10px; pointer-events: all; - cursor: ew-resize; z-index: 100; } .collectionStackedTimeline-resizer { right: 0; + cursor: e-resize; } .collectionStackedTimeline-left-resizer { left: 0; + cursor: w-resize; } } diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 2543624d3..b29abf083 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -244,6 +244,7 @@ export class CollectionStackedTimeline extends CollectionSubView DocumentManager.Instance.getDocumentView(anchor)?.select(false)); } (!isClick || !wasSelecting) && (this._markerEnd = undefined); + this._timelineWrapper && (this._timelineWrapper.style.cursor = ''); }), (e, doubleTap) => { if (e.button !== 2) { @@ -561,7 +563,7 @@ export class CollectionStackedTimeline extends CollectionSubView
e.stopPropagation()} onScroll={this.setScroll} onMouseMove={e => this.isContentActive() && this.onHover(e)} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 41e4d6b6a..e0f5cbe5b 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -1,16 +1,14 @@ - .marqueeView { position: inherit; - top:0; - left:0; - width:100%; - height:100%; + top: 0; + left: 0; + width: 100%; + height: 100%; overflow: hidden; border-radius: inherit; user-select: none; } - .marqueeView:focus-within { overflow: hidden; } @@ -22,13 +20,13 @@ border-color: black; pointer-events: none; .marquee-legend { - bottom:-18px; - left:0; + bottom: -18px; + left: 0; position: absolute; font-size: 9; - white-space:nowrap; + white-space: nowrap; } .marquee-legend::after { - content: "Press for lasso" + content: 'Press for lasso'; } -} \ No newline at end of file +} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index bfb8c1528..5a3594ffc 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -551,7 +551,7 @@ export class VideoBox extends ViewBoxAnnotatableComponentLoading
) : ( -
+
this._fullScreen && e.stopPropagation()}> {this._fullScreen && (
{ - if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._viewScale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, -- cgit v1.2.3-70-g09d2 From 4315a0378bc54ae9eaa684d416839f635c38e865 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 12 Sep 2022 16:18:42 -0400 Subject: fixed rotating UI for video boxes. fixed generating error response for unsupported video/audio formats. --- src/client/views/DocumentDecorations.tsx | 8 +++---- src/client/views/nodes/VideoBox.tsx | 38 +++++++++++++++++++++----------- src/server/DashUploadUtils.ts | 3 +++ 3 files changed, 31 insertions(+), 18 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index a79f727a7..832d0a35c 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -738,11 +738,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P )} - {useRotation && ( -
e.preventDefault()}> - {'⟲'} -
- )} +
e.preventDefault()}> + {'⟲'} +
{useRounding && (
{ - const bounds = this.props.docViewPath().lastElement().getBounds(); - const left = bounds?.left || 0; - const right = bounds?.right || 0; - const top = bounds?.top || 0; - const height = (bounds?.bottom || 0) - top; - const width = Math.max(right - left, 100); - const uiHeight = Math.max(25, Math.min(50, height / 10)); - const uiMargin = Math.min(10, height / 20); - const vidHeight = (height * this.heightPercent) / 100; - const yPos = top + vidHeight - uiHeight - uiMargin; - const xPos = uiHeight / vidHeight > 0.4 ? right + 10 : left + 10; + const xf = this.props.ScreenToLocalTransform().inverse(); + const height = this.props.PanelHeight(); + const vidHeight = (height * this.heightPercent) / 100 / this.scaling(); + const vidWidth = this.props.PanelWidth() / this.scaling(); + const uiHeight = 25; + const uiMargin = 10; + const yBot = xf.transformPoint(0, vidHeight)[1]; + // prettier-ignore + const yMid = (xf.transformPoint(0, 0)[1] + + xf.transformPoint(0, height / this.scaling())[1]) / 2; + const xPos = xf.transformPoint(vidWidth / 2, 0)[0]; + const xRight = xf.transformPoint(vidWidth, 0)[0]; const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0; - return this._fullScreen || right - left < 50 ? null : ( + return this._fullScreen || (xRight - xPos) * 2 < 50 ? null : (
-
+
{this.UIButtons}
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index e94ef8534..8cf657da4 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -142,6 +142,7 @@ export namespace DashUploadUtils { const result = await UploadImage(path, basename(path)); return { source: file, result }; } + return { source: file, result: { name: 'Unsupported image format', message: `Could not upload unsupported file (${name}). Please convert to an .jpg` } }; case 'video': if (format.includes('x-matroska')) { console.log('case video'); @@ -179,6 +180,7 @@ export namespace DashUploadUtils { if (videoFormats.includes(format)) { return MoveParsedFile(file, Directory.videos); } + return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${name}). Please convert to an .mp4` } }; case 'application': if (applicationFormats.includes(format)) { return UploadPdf(file); @@ -191,6 +193,7 @@ export namespace DashUploadUtils { if (audioFormats.includes(format)) { return UploadAudio(file, format); } + return { source: file, result: { name: 'Unsupported audio format', message: `Could not upload unsupported file (${name}). Please convert to an .mp3` } }; case 'text': if (types[1] == 'csv') { return UploadCsv(file); -- cgit v1.2.3-70-g09d2 From 0046f6eab6ec28b509398b5184922296014a66d0 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 21 Sep 2022 13:24:02 -0400 Subject: added cropping for videos. made images render at full res when selected. --- src/client/views/nodes/ImageBox.tsx | 2 +- src/client/views/nodes/VideoBox.tsx | 53 +++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 11f221448..d3d68d835 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -74,7 +74,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent (this._curSuffix = this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 || !selected ? '_l' : '_o'), + ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'), { fireImmediately: true, delay: 1000 } ); this._disposers.path = reaction( diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index b45ee7f6e..ac472aa89 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -30,6 +30,7 @@ import { StyleProp } from '../StyleProvider'; import { FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import './VideoBox.scss'; +import { ObjectField } from '../../../fields/ObjectField'; const path = require('path'); /** @@ -397,7 +398,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent { const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1); - if (aspect) { + if (aspect && !this.dataDoc.viewScaleMin) { Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth); Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight); this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect; @@ -570,7 +571,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent this.Play()} @@ -986,6 +987,53 @@ export class VideoBox extends ViewBoxAnnotatableComponent; } + crop = (region: Doc | undefined, addCrop?: boolean) => { + if (!region) return; + const cropping = Doc.MakeCopy(region, true); + Doc.GetProto(region).backgroundColor = 'transparent'; + Doc.GetProto(region).lockedPosition = true; + Doc.GetProto(region).title = 'region:' + this.rootDoc.title; + Doc.GetProto(region).isPushpin = true; + region._timecodeToHide = region._timecodeToShow; + this.addDocument(region); + const anchx = NumCast(cropping.x); + const anchy = NumCast(cropping.y); + const anchw = NumCast(cropping._width); + const anchh = NumCast(cropping._height); + const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw; + cropping.title = 'crop: ' + this.rootDoc.title; + cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); + cropping.y = NumCast(this.rootDoc.y); + cropping._width = anchw * (this.props.NativeDimScaling?.() || 1); + cropping._height = anchh * (this.props.NativeDimScaling?.() || 1); + cropping.timecodeToHide = undefined; + cropping.timecodeToShow = undefined; + cropping.isLinkButton = undefined; + const croppingProto = Doc.GetProto(cropping); + croppingProto.annotationOn = undefined; + croppingProto.isPrototype = true; + croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO + croppingProto.type = DocumentType.VID; + croppingProto.layout = VideoBox.LayoutString('data'); + croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField); + croppingProto['data-nativeWidth'] = anchw; + croppingProto['data-nativeHeight'] = anchh; + croppingProto.currentTimecode = this.layoutDoc._currentTimecode; + croppingProto.viewScale = viewScale; + croppingProto.viewScaleMin = viewScale; + croppingProto.panX = anchx / viewScale; + croppingProto.panY = anchy / viewScale; + croppingProto.panXMin = anchx / viewScale; + croppingProto.panXMax = anchw / viewScale; + croppingProto.panYMin = anchy / viewScale; + croppingProto.panYMax = anchh / viewScale; + if (addCrop) { + DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', ''); + } + this.props.bringToFront(cropping); + return cropping; + }; + savedAnnotations = () => this._savedAnnotations; render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); @@ -1048,6 +1096,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent )} {this.renderTimeline} -- cgit v1.2.3-70-g09d2 From 4484aff465c8f4f242b98d31558483eff245ab95 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 21 Sep 2022 13:31:20 -0400 Subject: no video ui for cropped videos --- src/client/views/nodes/VideoBox.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index ac472aa89..aabe3eb25 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -398,7 +398,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent { const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1); - if (aspect && !this.dataDoc.viewScaleMin) { + if (aspect && !this.isCropped) { Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth); Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight); this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect; @@ -571,7 +571,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent this.Play()} @@ -928,7 +928,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent
; } + @computed get isCropped() { + return this.dataDoc.viewScaleMin; // bcz: hack to identify a cropped video + } crop = (region: Doc | undefined, addCrop?: boolean) => { if (!region) return; const cropping = Doc.MakeCopy(region, true); -- cgit v1.2.3-70-g09d2 From 69719257b8275cb19d63960a6ec552a0c118d988 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 21 Sep 2022 16:43:54 -0400 Subject: fixed navigating to cropped video regions. --- .../collections/CollectionStackedTimeline.tsx | 27 ++++++++++++++++++++-- src/client/views/nodes/VideoBox.tsx | 17 ++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index b29abf083..c694e17fb 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -2,7 +2,7 @@ import React = require('react'); import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; -import { Doc, DocListCast, StrListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; @@ -23,11 +23,13 @@ import { AudioWaveform } from '../AudioWaveform'; import { CollectionSubView } from '../collections/CollectionSubView'; import { Colors } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; -import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps, DocumentViewSharedProps } from '../nodes/DocumentView'; import { LabelBox } from '../nodes/LabelBox'; import './CollectionStackedTimeline.scss'; import { VideoBox } from '../nodes/VideoBox'; import { ImageField } from '../../../fields/URLField'; +import { StyleProp } from '../StyleProvider'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; export type CollectionStackedTimelineProps = { Play: () => void; @@ -693,6 +695,7 @@ interface StackedTimelineAnchorProps { width: number; height: number; toTimeline: (screen_delta: number, width: number) => number; + styleProvider?: (doc: Opt, props: Opt, property: string) => any; playLink: (linkDoc: Doc) => void; setTime: (time: number) => void; startTag: string; @@ -803,6 +806,25 @@ class StackedTimelineAnchor extends React.Component return [resetTitle]; }; + innerStyleProvider = (doc: Opt, props: Opt, property: string): any => { + if (property === StyleProp.Decorations && doc && NumCast(doc.timecodeToHide) - NumCast(doc.timecodeToShow) < 0.0002) { + return ( +
+ { + LinkFollower.FollowLink(undefined, doc, props as DocumentViewSharedProps, e.altKey); + e.stopPropagation(); + }} + size="lg" + /> +
+ ); + } + return this.props.styleProvider?.(doc, props, property); + }; + // renders anchor LabelBox renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) { const anchor = observable({ view: undefined as any }); @@ -819,6 +841,7 @@ class StackedTimelineAnchor extends React.Component ref={action((r: DocumentView | null) => (anchor.view = r))} Document={mark} DataDoc={undefined} + styleProvider={this.innerStyleProvider} renderDepth={this.props.renderDepth + 1} LayoutTemplate={undefined} LayoutTemplateString={LabelBox.LayoutStringWithTitle('data', this.computeTitle())} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index aabe3eb25..4bcd79641 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -948,6 +948,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent { + if (doc !== this.rootDoc) { + const showTime = Cast(doc._timecodeToShow, 'number', null); + showTime !== undefined && setTimeout(() => this.Seek(showTime), 100); + return 0.1; + } + }; + // renders CollectionStackedTimeline @computed get renderTimeline() { return ( @@ -981,15 +989,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent ); } + @computed get isCropped() { + return this.dataDoc.videoCrop; // bcz: hack to identify a cropped video + } // renders annotation layer @computed get annotationLayer() { return
; } - @computed get isCropped() { - return this.dataDoc.viewScaleMin; // bcz: hack to identify a cropped video - } crop = (region: Doc | undefined, addCrop?: boolean) => { if (!region) return; const cropping = Doc.MakeCopy(region, true); @@ -997,7 +1005,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent Date: Wed, 21 Sep 2022 23:23:37 -0400 Subject: fixed video duration in presbox media controls --- src/client/views/nodes/VideoBox.tsx | 1 + src/client/views/nodes/trails/PresBox.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 4bcd79641..f7f558bb4 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -405,6 +405,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent() { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; const clipStart: number = NumCast(activeItem.clipStart); - const clipEnd: number = NumCast(activeItem.clipEnd); + const clipEnd: number = NumCast(activeItem.clipEnd, NumCast(activeItem.duration)); const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc); if (activeItem && targetDoc) { return ( -- cgit v1.2.3-70-g09d2 From 2fc88a931cb2fc3408297b000208990633445585 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 27 Oct 2022 15:57:09 -0400 Subject: cleaned up treeViews to work across PresBox/ppt slides/collections/ - modified DocumentView rendering to remove pres box refs. fixed mainView width. --- src/client/documents/Documents.ts | 4 +- src/client/util/CurrentUserUtils.ts | 8 +- src/client/views/DocumentDecorations.tsx | 18 +- src/client/views/LightboxView.tsx | 26 +-- src/client/views/MainView.tsx | 1 + src/client/views/StyleProvider.tsx | 5 +- .../views/collections/CollectionStackingView.tsx | 2 + .../views/collections/CollectionTreeView.scss | 2 - .../views/collections/CollectionTreeView.tsx | 62 ++++--- src/client/views/collections/CollectionView.tsx | 2 + src/client/views/collections/TabDocView.tsx | 1 - src/client/views/collections/TreeView.scss | 3 +- src/client/views/collections/TreeView.tsx | 179 +++++++++---------- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 6 +- src/client/views/nodes/DocumentView.tsx | 37 ++-- src/client/views/nodes/FieldView.tsx | 2 - src/client/views/nodes/VideoBox.tsx | 2 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 8 +- src/client/views/nodes/trails/PresBox.tsx | 7 +- src/client/views/nodes/trails/PresElementBox.scss | 14 +- src/client/views/nodes/trails/PresElementBox.tsx | 197 ++++++++++++--------- src/fields/Doc.ts | 3 + 23 files changed, 301 insertions(+), 290 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 2d82ffdf9..bbb896d6e 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -335,7 +335,6 @@ export class DocumentOptions { treeViewHideHeaderIfTemplate?: boolean; // whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox) treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items. - treeViewGrowsHorizontally?: boolean; // whether an embedded tree view of the document can grow horizontally without growing vertically treeViewChildDoubleClick?: ScriptField; // // Action Button buttonMenu?: boolean; // whether a action button should be displayed @@ -395,7 +394,6 @@ export namespace Docs { _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, - treeViewGrowsHorizontally: true, nativeHeightUnfrozen: true, forceReflow: true, links: '@links(self)', @@ -570,7 +568,7 @@ export namespace Docs { DocumentType.SLIDER, { layout: { view: SliderBox, dataField: defaultDataKey }, - options: { links: '@links(self)', treeViewGrowsHorizontally: true }, + options: { links: '@links(self)' }, }, ], [ diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 2bc464127..a2974177e 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -480,7 +480,7 @@ export class CurrentUserUtils { const childContextMenuIcons = ["chalkboard", "tv", "camera", "users", "times", "trash"]; // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters const reqdOpts:DocumentOptions = { title: "My Dashboards", childHideLinkButton: true, freezeChildren: "remove|add", treeViewHideTitle: true, boxShadow: "0 0", childDontRegisterViews: true, - targetDropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, system: true, treeViewTruncateTitleWidth: 150, ignoreClick: true, + targetDropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, system: true, treeViewTruncateTitleWidth: 350, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newDashboardButton, childDropAction: "alias", _showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true, contextMenuLabels:new List(contextMenuLabels), @@ -518,7 +518,7 @@ export class CurrentUserUtils { const reqdOpts:DocumentOptions = { _showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true, title: "My Documents", buttonMenu: true, buttonMenuDoc: newFolderButton, treeViewHideTitle: true, targetDropAction: "proto", system: true, isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, boxShadow: "0 0", childDontRegisterViews: true, - treeViewTruncateTitleWidth: 150, ignoreClick: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 350, ignoreClick: true, childDropAction: "alias", childContextMenuLabels: new List(["Create new folder"]), childContextMenuIcons: new List(["plus"]), explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard." @@ -535,7 +535,7 @@ export class CurrentUserUtils { static setupRecentlyClosed(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { _showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true, title: "My Recently Closed", buttonMenu: true, childHideLinkButton: true, treeViewHideTitle: true, childDropAction: "alias", system: true, - treeViewTruncateTitleWidth: 150, ignoreClick: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", + treeViewTruncateTitleWidth: 350, ignoreClick: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", contextMenuLabels: new List(["Empty recently closed"]), contextMenuIcons:new List(["trash"]), explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list." @@ -561,7 +561,7 @@ export class CurrentUserUtils { const reqdOpts:DocumentOptions = { _lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view", boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, system: true, - treeViewHideTitle: true, treeViewTruncateTitleWidth: 150 + treeViewHideTitle: true, treeViewTruncateTitleWidth: 350 }; if (!doc[field]) DocUtils.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" }); return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [doc]); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 10517d829..06eb6c6d7 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -112,7 +112,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } } //@ts-ignore - const titleField = +this._accumulatedTitle === this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle; + const titleField = +this._accumulatedTitle == this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle; Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); if (d.rootDoc.syncLayoutFieldWithTitle) { @@ -345,7 +345,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => { const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(rotCenter[0], rotCenter[1]); const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2]; - const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI); + const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI); seldocview.rootDoc.rotateCenterX = final.x / NumCast(seldocview.layoutDoc._width); seldocview.rootDoc.rotateCenterY = final.y / NumCast(seldocview.layoutDoc._height); }; @@ -381,7 +381,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const infos = new Map(); const seldocview = SelectionManager.Views()[0]; SelectionManager.Views().forEach(dv => { - const accumRot = (NumCast(dv.rootDoc._jitterRotation) / 180) * Math.PI; + const accumRot = (NumCast(dv.rootDoc._rotation) / 180) * Math.PI; const localRotCtr = dv.props.ScreenToLocalTransform().transformPoint(rcScreen.X, rcScreen.Y); const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.rootDoc.width) / 2, localRotCtr[1] - NumCast(dv.rootDoc.height) / 2]; const startRotCtr = Utils.rotPt(localRotCtrOffset[0], localRotCtrOffset[1], -accumRot); @@ -396,7 +396,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot: isAbs ? angle : accumRot + angle }); dv.rootDoc.x = infos.get(dv.rootDoc)!.unrotatedDocPos.x - (endRotCtr.x - startRotCtr.x); dv.rootDoc.y = infos.get(dv.rootDoc)!.unrotatedDocPos.y - (endRotCtr.y - startRotCtr.y); - dv.rootDoc._jitterRotation = ((isAbs ? 0 : NumCast(dv.rootDoc._jitterRotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360 + dv.rootDoc._rotation = ((isAbs ? 0 : NumCast(dv.rootDoc._rotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360 }) ); }; @@ -416,7 +416,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P return false; }, // moveEvent action(() => { - const oldRotation = NumCast(seldocview.rootDoc._jitterRotation); + const oldRotation = NumCast(seldocview.rootDoc._rotation); const diff = oldRotation - Math.round(oldRotation / 45) * 45; if (Math.abs(diff) < 5) { if (selectedInk.length) { @@ -611,7 +611,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } } else { const rotCtr = [NumCast(doc._width) / 2, NumCast(doc._height) / 2]; - const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._jitterRotation) / 180) * Math.PI); + const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._rotation) / 180) * Math.PI); const maxHeight = doc.nativHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling(); dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH); @@ -619,7 +619,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P dH && (doc._autoHeight = false); const rotCtr2 = [NumCast(doc._width) / 2, NumCast(doc._height) / 2]; - const tlRotated2 = Utils.rotPt(-rotCtr2[0], -rotCtr2[1], (NumCast(doc._jitterRotation) / 180) * Math.PI); + const tlRotated2 = Utils.rotPt(-rotCtr2[0], -rotCtr2[1], (NumCast(doc._rotation) / 180) * Math.PI); doc.x = NumCast(doc.x) + tlRotated.x + rotCtr[0] - (tlRotated2.x + rotCtr2[0]); // doc shifts by amount topleft moves because rotation is about center of doc doc.y = NumCast(doc.y) + tlRotated.y + rotCtr[1] - (tlRotated2.y + rotCtr2[1]); } @@ -695,7 +695,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const loccenter = Utils.rotPt( NumCast(seldocview.rootDoc.rotateCenterX) * NumCast(seldocview.layoutDoc._width), NumCast(seldocview.rootDoc.rotateCenterY) * NumCast(seldocview.layoutDoc._height), - (NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI + (NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI ); return seldocview.props .ScreenToLocalTransform() @@ -783,7 +783,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate? - const rotation = NumCast(seldocview.rootDoc._jitterRotation); + const rotation = NumCast(seldocview.rootDoc._rotation); const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : ''; diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index bd6cea28a..22b0380a2 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -289,29 +289,29 @@ export class LightboxView extends React.Component { })} Document={LightboxView.LightboxDoc} DataDoc={undefined} + PanelWidth={this.lightboxWidth} + PanelHeight={this.lightboxHeight} LayoutTemplate={LightboxView.LightboxDocTemplate} - addDocument={undefined} isDocumentActive={returnFalse} isContentActive={returnTrue} - addDocTab={this.addDocTab} - pinToPres={TabDocView.PinDoc} - onBrowseClick={MainView.Instance.exploreMode} + styleProvider={DefaultStyleProvider} + ScreenToLocalTransform={this.lightboxScreenToLocal} + renderDepth={0} rootSelected={returnTrue} docViewPath={returnEmptyDoclist} docFilters={this.docFilters} - removeDocument={undefined} - styleProvider={DefaultStyleProvider} - ScreenToLocalTransform={this.lightboxScreenToLocal} - PanelWidth={this.lightboxWidth} - PanelHeight={this.lightboxHeight} - focus={DocUtils.DefaultFocus} - whenChildContentsActiveChanged={emptyFunction} - bringToFront={emptyFunction} docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} - renderDepth={0} + addDocument={undefined} + removeDocument={undefined} + whenChildContentsActiveChanged={emptyFunction} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} + bringToFront={emptyFunction} + onBrowseClick={MainView.Instance.exploreMode} + focus={DocUtils.DefaultFocus} />
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 03c0642ed..4dc1ebd99 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -647,6 +647,7 @@ export class MainView extends React.Component { e.preventDefault(); }} style={{ + width: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`, minWidth: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`, transform: LightboxView.LightboxDoc ? 'scale(0.0001)' : undefined, }}> diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 2eb3dd532..bc8bd7b7f 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -85,7 +85,6 @@ export function DefaultStyleProvider(doc: Opt, props: Opt doc && !Doc.IsSystem(doc) && Doc.UserDoc().renderStyle === 'comic'; const isBackground = () => doc && BoolCast(doc._lockedPosition); const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor); const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); @@ -167,11 +166,11 @@ export function DefaultStyleProvider(doc: Opt, props: Opt (props?.PanelHeight() || 0) ? 5 : 10) : 0; + return Doc.IsComicStyle(doc) ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0; case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) || (doc?.type === DocumentType.RTF && !showTitle()?.includes('noMargin')) || diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index ba29b1d6f..77b47ed82 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -350,6 +350,8 @@ export class CollectionStackingView extends CollectionSubView = new Set(); // list of tree view items to monitor for height changes private observer: any; // observer for monitoring tree view items. - private static expandViewLabelSize = 20; @computed get doc() { return this.props.Document; @@ -100,7 +99,10 @@ export class CollectionTreeView extends CollectionSubView disposer?.()); } + shrinkWrap = () => {}; // placeholder to allow setContentView to work + componentDidMount() { + //this.props.setContentView?.(this); this._disposers.autoheight = reaction( () => this.rootDoc.autoHeight, auto => auto && this.computeHeight(), @@ -197,9 +199,7 @@ export class CollectionTreeView extends CollectionSubView void) => this.onExternalDrop(e, {}, addDocs); @undoBatch - makeTextCollection = (childDocs: Doc[]) => { - this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true); - }; + makeTextCollection = (childDocs: Doc[]) => this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true); get editableTitle() { return ( @@ -258,6 +258,7 @@ export class CollectionTreeView extends CollectionSubView ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label })); }; + headerFields = () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields); @computed get treeViewElements() { TraceMobx(); const dropAction = StrCast(this.doc.childDropAction) as dropActionType; @@ -281,7 +282,7 @@ export class CollectionTreeView extends CollectionSubView this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields), + this.headerFields, [], this.props.onCheckedClick, this.onChildClick, @@ -304,7 +305,7 @@ export class CollectionTreeView extends CollectionSubView (this._titleRef = r) && (this._titleHeight = r.getBoundingClientRect().height * this.props.ScreenToLocalTransform().Scale))} key={this.doc[Id]} - style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}}> + style={!this.outlineMode ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}}> {this.outlineMode ? this.documentTitle : this.editableTitle}
); @@ -372,7 +373,7 @@ export class CollectionTreeView extends CollectionSubView (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins); truncateTitleWidth = () => this.treeViewtruncateTitleWidth; onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); - panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.NativeDimScaling?.() || 1); + panelWidth = () => Math.max(0, this.props.PanelWidth() - 2 * this.marginX() * (this.props.NativeDimScaling?.() || 1)); addAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.addDocument(doc, `${this.props.fieldKey}-annotations`) || false; remAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false; @@ -396,7 +397,7 @@ export class CollectionTreeView extends CollectionSubView @@ -432,24 +431,29 @@ export class CollectionTreeView extends CollectionSubView - {this.contentFunc} - - ) : ( - this.contentFunc() + const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1) || 1; + return ( +
+ {!(this.doc instanceof Doc) || !this.treeChildren ? null : this.doc.treeViewHasOverlay ? ( + + {this.contentFunc} + + ) : ( + this.contentFunc() + )} +
); } } diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index dcaad5632..9f63a11aa 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -54,6 +54,8 @@ interface CollectionViewProps_ extends FieldViewProps { childHideDecorationTitle?: () => boolean; childHideResizeHandles?: () => boolean; childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection + childXPadding?: number; + childYPadding?: number; childLayoutString?: string; childIgnoreNativeSize?: boolean; childClickScript?: ScriptField; diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index a6cfbd6b2..cde5132a3 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -256,7 +256,6 @@ export class TabDocView extends React.Component { pinDoc.treeViewChildrenOnRoot = true; // tree view will look for hierarchical children on the root doc, not the data doc. pinDoc.treeViewFieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field pinDoc.treeViewExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view - pinDoc.treeViewGrowsHorizontally = true; // the document expands horizontally when displayed as a tree view header pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header const presArray: Doc[] = PresBox.Instance?.sortArray(); const size: number = PresBox.Instance?.selectedArray.size; diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index 57bb5274d..83fee013a 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -24,6 +24,7 @@ // width: $TREE_BULLET_WIDTH; width: 100%; height: 100%; + position: absolute; .treeView-expandIcon { display: none; @@ -118,7 +119,7 @@ display: flex; // needed for PresBox's treeView border: transparent 1px solid; align-items: center; - width: 100%; + width: max-content; border-radius: 5px; &:hover { diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index b9f86fa6b..1e97eee37 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -10,7 +10,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, simulateMouseClick, Utils } from '../../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; @@ -321,6 +321,7 @@ export class TreeView extends React.Component { _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, + _fitWidth: true, treeViewType: TreeViewType.outline, x: 0, y: 0, @@ -398,29 +399,22 @@ export class TreeView extends React.Component { }; docTransform = () => this.refTransform(this._dref?.ContentRef?.current); getTransform = () => this.refTransform(this._tref.current); - docWidth = () => { - const layoutDoc = this.layoutDoc; - const aspect = Doc.NativeAspect(layoutDoc); - if (layoutDoc._fitWidth) return Math.min(this.props.panelWidth() - treeBulletWidth(), layoutDoc[WidthSym]()); - if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.panelWidth() - treeBulletWidth())); - return Math.min((this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.NativeDimScaling?.() || 1), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]()); - }; - docHeight = () => { - const layoutDoc = this.layoutDoc; - return Math.max( - 70, - Math.min( - this.MAX_EMBED_HEIGHT, - (() => { - const aspect = Doc.NativeAspect(layoutDoc); - if (aspect) return this.docWidth() / (aspect || 1); - return layoutDoc._fitWidth - ? !Doc.NativeHeight(this.doc) - ? NumCast(this.props.containerCollection._height) - : Math.min((this.docWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.containerCollection._height))) - : layoutDoc[HeightSym]() || 50; - })() - ) + embeddedPanelWidth = () => this.props.panelWidth() / (this.props.treeView.props.NativeDimScaling?.() || 1); + embeddedPanelHeight = () => { + console.log(this.props.treeView.rootDoc.title); + const layoutDoc = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc; + return Math.min( + layoutDoc[HeightSym](), + this.MAX_EMBED_HEIGHT, + (() => { + const aspect = Doc.NativeAspect(layoutDoc); + if (aspect) return this.embeddedPanelWidth() / (aspect || 1); + return layoutDoc._fitWidth + ? !Doc.NativeHeight(layoutDoc) + ? NumCast(layoutDoc._height) //this.props.containerCollection._height) + : Math.min((this.embeddedPanelWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.containerCollection._height))) + : (this.embeddedPanelWidth() * layoutDoc[HeightSym]()) / layoutDoc[WidthSym](); + })() ); }; @@ -512,28 +506,6 @@ export class TreeView extends React.Component { return rows; } - rtfWidth = () => { - const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc; - return Math.min(layout[WidthSym](), this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.NativeDimScaling?.() || 1); - }; - rtfHeight = () => { - const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc; - return this.rtfWidth() <= layout[WidthSym]() ? Math.min(layout[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; - }; - rtfOutlineHeight = () => Math.max(this.layoutDoc?.[HeightSym](), treeBulletWidth()); - expandPanelHeight = () => { - if (this.layoutDoc._fitWidth) return this.docHeight(); - const aspect = this.layoutDoc[WidthSym]() / this.layoutDoc[HeightSym](); - const docAspect = this.docWidth() / this.docHeight(); - return docAspect < aspect ? this.docWidth() / aspect : this.docHeight(); - }; - expandPanelWidth = () => { - if (this.layoutDoc._fitWidth) return this.docWidth(); - const aspect = this.layoutDoc[WidthSym]() / this.layoutDoc[HeightSym](); - const docAspect = this.docWidth() / this.docHeight(); - return docAspect > aspect ? this.docHeight() * aspect : this.docWidth(); - }; - @computed get renderContent() { TraceMobx(); const expandKey = this.treeViewExpandedView; @@ -792,15 +764,18 @@ export class TreeView extends React.Component { titleStyleProvider = (doc: Doc | undefined, props: Opt, property: string): any => { if (!doc || doc !== this.doc) return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView + const treeView = this.props.treeView; // prettier-ignore switch (property.split(':')[0]) { case StyleProp.Opacity: return this.props.treeView.outlineMode ? undefined : 1; case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor)); case StyleProp.Highlighting: if (this.props.treeView.outlineMode) return undefined; + case StyleProp.Hidden: return false; + case StyleProp.BoxShadow: return undefined; case StyleProp.DocContents: const highlightIndex = this.props.treeView.outlineMode ? Doc.DocBrushStatus.unbrushed : Doc.isBrushedHighlightedDegree(doc); const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; - return this.props.treeView.outlineMode ? null : ( + return treeView.outlineMode ? null : (
{ maxWidth: props?.PanelWidth() || undefined, background: props?.styleProvider?.(doc, props, StyleProp.BackgroundColor), outline: `solid ${highlightColor} ${highlightIndex}px`, + paddingLeft: NumCast(treeView.rootDoc.childXPadding, NumCast(treeView.props.childXPadding, Doc.IsComicStyle(doc)?20:0)), + paddingRight: NumCast(treeView.rootDoc.childXPadding, NumCast(treeView.props.childXPadding, Doc.IsComicStyle(doc)?20:0)), + paddingTop: treeView.props.childYPadding, + paddingBottom: treeView.props.childYPadding, }}> {StrCast(doc?.title)}
); } - return this.props?.treeView?.props.styleProvider?.(doc, props, property); + return treeView.props.styleProvider?.(doc, props, property); }; embeddedStyleProvider = (doc: Doc | undefined, props: Opt, property: string): any => { if (property.startsWith(StyleProp.Decorations)) return null; + if (property.startsWith(StyleProp.Hidden)) return false; return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView }; onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { @@ -843,7 +823,7 @@ export class TreeView extends React.Component { } return false; }; - titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth() - 2 * treeBulletWidth())); + titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth())) / (this.props.treeView.props.NativeDimScaling?.() || 1) - 3 * treeBulletWidth(); return18 = () => 18; /** @@ -851,6 +831,7 @@ export class TreeView extends React.Component { */ @computed get renderTitle() { + // TraceMobx(); const view = this._editTitle ? ( { } })} Document={this.doc} + fitWidth={(doc: Doc) => true} DataDoc={undefined} scriptContext={this} hideDecorationTitle={this.props.treeView.outlineMode} @@ -905,7 +887,7 @@ export class TreeView extends React.Component { removeDocument={this.props.removeDoc} ScreenToLocalTransform={this.getTransform} NativeHeight={this.return18} - NativeWidth={this.titleWidth} + NativeWidth={returnZero} PanelWidth={this.titleWidth} PanelHeight={this.return18} contextMenuItems={this.contextMenuItems} @@ -918,10 +900,12 @@ export class TreeView extends React.Component { disableDocBrushing={this.props.treeView.props.disableDocBrushing} hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)} dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)} + xPadding={NumCast(this.props.treeView.props.Document.childXPadding, this.props.treeView.props.childXPadding)} + yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} - ContainingCollectionView={undefined} + ContainingCollectionView={this.props.treeView.props.CollectionView} ContainingCollectionDoc={this.props.treeView.props.Document} /> ); @@ -968,53 +952,52 @@ export class TreeView extends React.Component { }; renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => { - const isExpandable = this.doc._treeViewGrowsHorizontally; - const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth; - const panelHeight = asText ? this.rtfOutlineHeight : isExpandable ? this.rtfHeight : this.expandPanelHeight; return ( - (this._dref = r))} - Document={this.doc} - DataDoc={undefined} - PanelWidth={panelWidth} - PanelHeight={panelHeight} - NativeWidth={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfWidth : undefined} - NativeHeight={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfHeight : undefined} - LayoutTemplateString={asText ? FormattedTextBox.LayoutString('text') : undefined} - LayoutTemplate={this.props.treeView.props.childLayoutTemplate} - isContentActive={isActive} - isDocumentActive={isActive} - styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider} - hideTitle={asText} - fitContentsToBox={returnTrue} - hideDecorationTitle={this.props.treeView.outlineMode} - hideResizeHandles={this.props.treeView.outlineMode} - onClick={this.onChildClick} - focus={this.refocus} - onKey={this.onKeyDown} - hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)} - dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)} - ScreenToLocalTransform={this.docTransform} - renderDepth={this.props.renderDepth + 1} - treeViewDoc={this.props.treeView?.props.Document} - rootSelected={returnTrue} - docViewPath={this.props.treeView.props.docViewPath} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionDoc={this.props.containerCollection} - ContainingCollectionView={undefined} - addDocument={this.props.addDocument} - moveDocument={this.move} - removeDocument={this.props.removeDoc} - whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.props.addDocTab} - pinToPres={this.props.treeView.props.pinToPres} - disableDocBrushing={this.props.treeView.props.disableDocBrushing} - bringToFront={returnFalse} - scriptContext={this} - /> +
+ (this._dref = r))} + Document={this.doc} + DataDoc={undefined} + PanelWidth={this.embeddedPanelWidth} + PanelHeight={this.embeddedPanelHeight} + LayoutTemplateString={asText ? FormattedTextBox.LayoutString('text') : undefined} + LayoutTemplate={this.props.treeView.props.childLayoutTemplate} + isContentActive={isActive} + isDocumentActive={isActive} + styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider} + hideTitle={asText} + //fitContentsToBox={returnTrue} + hideDecorationTitle={this.props.treeView.outlineMode} + hideResizeHandles={this.props.treeView.outlineMode} + onClick={this.onChildClick} + focus={this.refocus} + onKey={this.onKeyDown} + hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)} + dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)} + ScreenToLocalTransform={this.docTransform} + renderDepth={this.props.renderDepth + 1} + treeViewDoc={this.props.treeView?.props.Document} + rootSelected={returnTrue} + docViewPath={this.props.treeView.props.docViewPath} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.props.containerCollection} + ContainingCollectionView={undefined} + addDocument={this.props.addDocument} + moveDocument={this.move} + removeDocument={this.props.removeDoc} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + xPadding={NumCast(this.props.treeView.props.Document.childXPadding, this.props.treeView.props.childXPadding)} + yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)} + addDocTab={this.props.addDocTab} + pinToPres={this.props.treeView.props.pinToPres} + disableDocBrushing={this.props.treeView.props.disableDocBrushing} + bringToFront={returnFalse} + scriptContext={this} + /> +
); }; @@ -1148,7 +1131,7 @@ export class TreeView extends React.Component { } const docs = TreeView.sortDocs(childDocs, StrCast(containerCollection.treeViewSortCriterion, TreeSort.None)); - const rowWidth = () => panelWidth() - treeBulletWidth(); + const rowWidth = () => panelWidth() - treeBulletWidth() * (treeView.props.NativeDimScaling?.() || 1); const treeViewRefs = new Map(); return docs .filter(child => child instanceof Doc) diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 559951efc..2f246e74f 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1317,7 +1317,7 @@ export class CollectionFreeFormView extends CollectionSubView ); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 04c7b96e3..260c98816 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -25,7 +25,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { renderCutoffProvider: (doc: Doc) => boolean; zIndex?: number; highlight?: boolean; - jitterRotation: number; + rotation: number; dataTransition?: string; replica: string; CollectionFreeFormView: CollectionFreeFormView; @@ -38,7 +38,7 @@ export class CollectionFreeFormDocumentView extends DocComponent void; ScreenToLocalTransform: () => Transform; bringToFront: (doc: Doc, sendToBack?: boolean) => void; + xPadding?: number; + yPadding?: number; dropAction?: dropActionType; dontRegisterView?: boolean; hideLinkButton?: boolean; @@ -1349,17 +1352,10 @@ export class DocumentViewInternal extends DocComponent - {!this.headerMargin ? ( - <> - {' '} - {this.contents} {titleView}{' '} - - ) : ( - <> - {' '} - {titleView} {this.contents}{' '} - - )} + {' '} + {!this.headerMargin ? this.contents : titleView} + {!this.headerMargin ? titleView : this.contents} + {' ' /* */} {captionView}
); @@ -1372,7 +1368,7 @@ export class DocumentViewInternal extends DocComponent {animRenderDoc}
- +
@@ -1552,7 +1548,7 @@ export class DocumentView extends React.Component { return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth)); } @computed get shouldNotScale() { - return (this.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any); + return (this.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || [CollectionViewType.Docking].includes(this.Document._viewType as any); } @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width); @@ -1597,7 +1593,7 @@ export class DocumentView extends React.Component { toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); focus = (doc: Doc, options: DocFocusOptions) => this.docView?.focus(doc, options); getBounds = () => { - if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { + if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { return undefined; } const xf = this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling).inverse(); @@ -1687,10 +1683,9 @@ export class DocumentView extends React.Component { render() { TraceMobx(); - const xshift = () => (Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); - const yshift = () => (Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); - const isPresTreeElement: boolean = this.props.treeViewDoc?.type === DocumentType.PRES; - const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; + const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined; + const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined; + const isButton = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; return (
{!this.props.Document || !this.props.PanelWidth() ? null : ( @@ -1700,11 +1695,11 @@ export class DocumentView extends React.Component { style={{ transition: this.props.dataTransition, transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, - width: isButton || isPresTreeElement ? '100%' : xshift() ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, + width: isButton ? '100%' : xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, height: isButton || this.props.forceAutoHeight ? undefined - : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`), + : yshift ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`), }}> this.autoHeight, autoHeight => autoHeight && this.tryUpdateScrollHeight() ); + this._disposers.width = reaction( + () => this.props.PanelWidth(), + width => this.tryUpdateScrollHeight() + ); this._disposers.scrollHeight = reaction( () => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }), - ({ width, scrollHeight, autoHeight }) => { - width && autoHeight && this.resetNativeHeight(scrollHeight); - }, + ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight), { fireImmediately: true } ); this._disposers.componentHeights = reaction( diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 258dad39c..4e8aed8a6 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -429,7 +429,7 @@ export class PresBox extends ViewBoxBaseComponent() { pinDoc.presPinLayout = true; pinDoc.presX = NumCast(targetDoc.x); pinDoc.presY = NumCast(targetDoc.y); - pinDoc.presRot = NumCast(targetDoc.jitterRotation); + pinDoc.presRot = NumCast(targetDoc.rotation); pinDoc.presWidth = NumCast(targetDoc.width); pinDoc.presHeight = NumCast(targetDoc.height); } @@ -521,7 +521,7 @@ export class PresBox extends ViewBoxBaseComponent() { targetDoc._dataTransition = presTransitionTime; targetDoc.x = NumCast(activeItem.presX, NumCast(targetDoc.x)); targetDoc.y = NumCast(activeItem.presY, NumCast(targetDoc.y)); - targetDoc.jitterRotation = NumCast(activeItem.presRot, NumCast(targetDoc.jitterRotation)); + targetDoc.rotation = NumCast(activeItem.presRot, NumCast(targetDoc.rotation)); targetDoc.width = NumCast(activeItem.presWidth, NumCast(targetDoc.width)); targetDoc.height = NumCast(activeItem.presHeight, NumCast(targetDoc.height)); setTimeout(() => (targetDoc._dataTransition = undefined), transTime + 10); @@ -2652,9 +2652,10 @@ export class PresBox extends ViewBoxBaseComponent() { PanelHeight={this.panelHeight} childIgnoreNativeSize={true} moveDocument={returnFalse} - childFitWidth={returnTrue} + //childFitWidth={returnTrue} childOpacity={returnOne} childLayoutTemplate={this.childLayoutTemplate} + childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined} filterAddDocument={this.addDocumentFilter} removeDocument={returnFalse} dontRegisterView={true} diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss index 8a6c2a6dc..415253af1 100644 --- a/src/client/views/nodes/trails/PresElementBox.scss +++ b/src/client/views/nodes/trails/PresElementBox.scss @@ -42,10 +42,7 @@ $slide-active: #5b9fdd; width: 100%; border-bottom: 0.5px solid grey; display: flex; - justify-content: space-between; align-items: center; - grid-template-rows: 16px 10px auto; - grid-template-columns: max-content max-content max-content max-content auto; .presItem-name { display: flex; @@ -102,13 +99,7 @@ $slide-active: #5b9fdd; grid-row: 3; grid-column: 1/8; position: relative; - display: flex; - width: auto; - justify-content: center; - margin: auto; - margin-bottom: 2px; - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; + display: inline-block; } .presItem-embeddedMask { @@ -124,8 +115,7 @@ $slide-active: #5b9fdd; .presItem-slideButtons { display: flex; - grid-column: 7; - grid-row: 1/3; + position: absolute; width: max-content; justify-self: right; justify-content: flex-end; diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index f4ab845f3..5d14a0e9a 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -4,7 +4,9 @@ import { action, computed, IReactionDisposer, observable, reaction } from 'mobx' import { observer } from 'mobx-react'; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; import { Copy, Id } from '../../../../fields/FieldSymbols'; +import { InkField } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; +import { RichTextField } from '../../../../fields/RichTextField'; import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -14,6 +16,7 @@ import { DragManager } from '../../../util/DragManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; +import { MarqueeView } from '../../collections/collectionFreeForm'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; @@ -24,9 +27,6 @@ import { PresBox } from './PresBox'; import './PresElementBox.scss'; import { PresMovement } from './PresEnums'; import React = require('react'); -import { InkField } from '../../../../fields/InkField'; -import { RichTextField } from '../../../../fields/RichTextField'; -import { MarqueeView } from '../../collections/collectionFreeForm'; /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. @@ -44,8 +44,11 @@ export class PresElementBox extends ViewBoxBaseComponent() { @computed get indexInPres() { return DocListCast(this.presBox[StrCast(this.presBox.presFieldKey, 'data')]).indexOf(this.rootDoc); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements) + @computed get expandViewHeight() { + return 100; + } @computed get collapsedHeight() { - return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(this.presBox._viewType as any) ? 35 : 31; + return 35; } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up @computed get presStatus() { return this.presBox.presStatus; @@ -58,7 +61,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { return vpath.length > 1 ? (vpath[vpath.length - 2].ComponentView as PresBox) : undefined; } @computed get presBox() { - return (this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc)!; + return this.props.ContainingCollectionDoc!; } @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; @@ -67,8 +70,8 @@ export class PresElementBox extends ViewBoxBaseComponent() { componentDidMount() { this.layoutDoc.hideLinkButton = true; this._heightDisposer = reaction( - () => [this.rootDoc.presExpandInlineButton, this.collapsedHeight], - params => (this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0)), + () => ({ expand: this.rootDoc.presExpandInlineButton, height: this.collapsedHeight }), + ({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)), { fireImmediately: true } ); } @@ -84,10 +87,10 @@ export class PresElementBox extends ViewBoxBaseComponent() { @action presExpandDocumentClick = () => (this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton); - embedHeight = (): number => 97; + embedHeight = (): number => this.collapsedHeight + this.expandViewHeight; // embedWidth = () => this.props.PanelWidth(); // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight); - embedWidth = (): number => this.props.PanelWidth() - 35; + embedWidth = (): number => this.props.PanelWidth() / 2; styleProvider = (doc: Doc | undefined, props: Opt, property: string): any => { if (property === StyleProp.Opacity) return 1; return this.props.styleProvider?.(doc, props, property); @@ -98,35 +101,35 @@ export class PresElementBox extends ViewBoxBaseComponent() { */ @computed get renderEmbeddedInline() { return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? null : ( -
+
-
+ {/*
*/}
); } @@ -206,8 +209,8 @@ export class PresElementBox extends ViewBoxBaseComponent() { const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []); if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc); dragData.dropAction = 'move'; - dragData.treeViewDoc = this.props.docViewPath().lastElement()?.props.treeViewDoc; - dragData.moveDocument = this.props.docViewPath().lastElement()?.props.moveDocument; + dragData.treeViewDoc = this.presBox._viewType === CollectionViewType.Tree ? this.props.ContainingCollectionDoc : undefined; // this.props.DocumentView?.()?.props.treeViewDoc; + dragData.moveDocument = this.props.moveDocument; const dragItem: HTMLElement[] = []; if (dragArray.length === 1) { const doc = this._itemRef.current || dragArray[0]; @@ -307,7 +310,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { updateCapturedContainerLayout = (targetDoc: Doc, activeItem: Doc) => { activeItem.presX = NumCast(targetDoc.x); activeItem.presY = NumCast(targetDoc.y); - activeItem.presRot = NumCast(targetDoc.jitterRotation); + activeItem.presRot = NumCast(targetDoc.rotation); activeItem.presWidth = NumCast(targetDoc.width); activeItem.presHeight = NumCast(targetDoc.height); }; @@ -464,16 +467,90 @@ export class PresElementBox extends ViewBoxBaseComponent() { return width; } + @computed get presButtons() { + const presBox: Doc = this.presBox; //presBox + const presBoxColor: string = StrCast(presBox._backgroundColor); + const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; + const targetDoc: Doc = this.targetDoc; + const activeItem: Doc = this.rootDoc; + + const items: JSX.Element[] = []; + if (activeItem.presPinLayout) { + items.push( + Update captured doc layout
}> +
this.updateCapturedContainerLayout(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}> + L +
+ + ); + } + if (activeItem.presPinData || activeItem.presPinView) { + items.push( + Update captured doc content
}> +
this.updateCapturedViewContents(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}> + C +
+ + ); + } + if (!Doc.noviceMode) { + items.push( + {this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}
}> +
(this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> + e.stopPropagation()} /> +
+ + ); + } + if (this.indexInPres === 0) { + items.push( + {activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}
}> +
(activeItem.groupWithUp = !activeItem.groupWithUp)} + style={{ + zIndex: 1000 - this.indexInPres, + fontWeight: 700, + backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined, + height: activeItem.groupWithUp ? 53 : 18, + transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined, + }}> +
+ e.stopPropagation()} /> +
+
+ + ); + } + items.push( + {this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}
}> +
{ + e.stopPropagation(); + this.presExpandDocumentClick(); + }}> + e.stopPropagation()} /> +
+ + ); + items.push( + Remove from presentation
}> +
+ e.stopPropagation()} /> +
+ + ); + return items; + } + @computed get mainItem() { const isSelected: boolean = this.selectedArray?.has(this.rootDoc) ? true : false; const isCurrent: boolean = this.presBox._itemIndex === this.indexInPres; - const toolbarWidth: number = this.toolbarWidth; - const showMore: boolean = this.toolbarWidth >= 300; const miniView: boolean = this.toolbarWidth <= 110; const presBox: Doc = this.presBox; //presBox const presBoxColor: string = StrCast(presBox._backgroundColor); const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; - const targetDoc: Doc = this.targetDoc; const activeItem: Doc = this.rootDoc; return ( @@ -484,6 +561,10 @@ export class PresElementBox extends ViewBoxBaseComponent() { style={{ backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent', opacity: this._dragging ? 0.3 : 1, + paddingLeft: NumCast(this.layoutDoc._xPadding, this.props.xPadding), + paddingRight: NumCast(this.layoutDoc._xPadding, this.props.xPadding), + paddingTop: NumCast(this.layoutDoc._yPadding, this.props.yPadding), + paddingBottom: NumCast(this.layoutDoc._yPadding, this.props.yPadding), }} onClick={e => { e.stopPropagation(); @@ -507,6 +588,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { ref={this._dragRef} className={`presItem-slide ${isCurrent ? 'active' : ''}`} style={{ + display: 'infline-block', backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), //boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, border: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? presBoxColor + ' solid 2.5px' : undefined) : undefined, @@ -514,8 +596,9 @@ export class PresElementBox extends ViewBoxBaseComponent() {
{`${this.indexInPres + 1}. `}
@@ -523,56 +606,8 @@ export class PresElementBox extends ViewBoxBaseComponent() {
{/*
{"Movement speed"}
}>
{this.transition}
*/} {/*
{"Duration"}
}>
{this.duration}
*/} -
- Update captured doc layout
}> -
this.updateCapturedContainerLayout(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinLayout ? 'flex' : 'none' }}> - L -
- - Update captured doc content
}> -
this.updateCapturedViewContents(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinData || activeItem.presPinView ? 'flex' : 'none' }}> - C -
- - {!Doc.noviceMode && ( - {this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}
}> -
(this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> - e.stopPropagation()} /> -
- - )} - {activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}
}> -
(activeItem.groupWithUp = !activeItem.groupWithUp)} - style={{ - display: this.indexInPres === 0 ? 'none' : '', - zIndex: 1000 - this.indexInPres, - fontWeight: 700, - backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined, - height: activeItem.groupWithUp ? 53 : 18, - transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined, - }}> -
- e.stopPropagation()} /> -
-
- - {this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}
}> -
{ - e.stopPropagation(); - this.presExpandDocumentClick(); - }}> - e.stopPropagation()} /> -
- - Remove from presentation
}> -
- e.stopPropagation()} /> -
- +
+ {...this.presButtons}
{this.renderEmbeddedInline} diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 466b43287..fc43325fe 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -238,6 +238,9 @@ export class Doc extends RefField { Doc.UserDoc().activePage = val; DocServer.UPDATE_SERVER_CACHE(); } + public static IsComicStyle(doc?: Doc) { + return doc && Doc.ActiveDashboard && !Doc.IsSystem(doc) && Doc.UserDoc().renderStyle === 'comic'; + } public static get ActiveDashboard() { return DocCast(Doc.UserDoc().activeDashboard); } -- cgit v1.2.3-70-g09d2 From dcc1c3cb204f5ffe506860daf772f2e978371738 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 2 Nov 2022 09:57:16 -0400 Subject: made decorations semi transparent until hover to make ui less noisy --- src/client/views/DocumentDecorations.scss | 31 +++++++++++++------ src/client/views/DocumentDecorations.tsx | 51 ++++++++++++++++--------------- src/client/views/nodes/VideoBox.tsx | 3 +- 3 files changed, 51 insertions(+), 34 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index fe1190e27..b41962c73 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -25,6 +25,10 @@ $resizeHandler: 8px; align-items: center; text-align: center; font-size: 30px; + opacity: 0.1; + &:hover { + opacity: 1; + } } .documentDecorations-rotationCenter { position: absolute; @@ -72,10 +76,12 @@ $resizeHandler: 8px; height: 20px; min-width: 20px; border-radius: 100%; + opacity: 0.5; cursor: pointer; &:hover { color: #02600d; + opacity: 1; } } @@ -93,10 +99,12 @@ $resizeHandler: 8px; height: 20px; min-width: 20px; border-radius: 100%; + opacity: 0.5; cursor: pointer; &:hover { color: #a94442; + opacity: 1; } > svg { @@ -120,10 +128,12 @@ $resizeHandler: 8px; height: 20px; min-width: 20px; border-radius: 100%; + opacity: 0.5; cursor: pointer; &:hover { color: #a94442; + opacity: 1; } > svg { @@ -144,6 +154,10 @@ $resizeHandler: 8px; border-radius: 8px; outline: none; border: none; + opacity: 0.3; + &:hover { + opacity: 1; + } .documentDecorations-titleSpan, .documentDecorations-titleSpan-Dark { @@ -206,7 +220,7 @@ $resizeHandler: 8px; .documentDecorations-rightResizer { pointer-events: auto; background: $medium-gray; - opacity: 0.1; + opacity: 0.2; &:hover { opacity: 1; } @@ -248,10 +262,8 @@ $resizeHandler: 8px; } .documentDecorations-lock { - position: absolute; + position: relative; background: black; - right: 23px; - top: 3px; color: gray; height: 14; width: 14; @@ -261,6 +273,7 @@ $resizeHandler: 8px; align-items: center; flex-direction: column; border-radius: 15%; + cursor: default; } .documentDecorations-rotationPath { @@ -277,7 +290,6 @@ $resizeHandler: 8px; .documentDecorations-bottomRightResizer { cursor: nwse-resize; background: unset; - opacity: 1; transform: scale(2); } @@ -317,7 +329,6 @@ $resizeHandler: 8px; .documentDecorations-bottomRightResizer { cursor: nwse-resize; background: unset; - opacity: 1; } .documentDecorations-topLeftResizer { @@ -333,7 +344,6 @@ $resizeHandler: 8px; .documentDecorations-topLeftResizer:hover, .documentDecorations-bottomRightResizer:hover { opacity: 1; - background: black; } .documentDecorations-bottomRightResizer { @@ -344,7 +354,6 @@ $resizeHandler: 8px; .documentDecorations-bottomLeftResizer { cursor: nesw-resize; background: unset; - opacity: 1; transform: scale(2); } @@ -360,7 +369,6 @@ $resizeHandler: 8px; .documentDecorations-topRightResizer:hover, .documentDecorations-bottomLeftResizer:hover { - background: black; opacity: 1; } @@ -414,6 +422,11 @@ $resizeHandler: 8px; gap: 5px; top: 4px; background: $light-gray; + opacity: 0.2; + pointer-events: all; + &:hover { + opacity: 1; + } } .linkButtonWrapper { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 8e0686038..47347284c 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -755,24 +755,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P ); const colorScheme = StrCast(Doc.ActiveDashboard?.colorScheme); - const titleArea = this._editingTitle ? ( - !hideTitle && this.titleBlur()} - onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))} - onKeyDown={hideTitle ? emptyFunction : this.titleEntered} - onPointerDown={e => e.stopPropagation()} - /> - ) : ( -
- {`${hideTitle ? '' : this.selectionTitle}`} -
- ); const leftBounds = this.props.boundsLeft; const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop; @@ -782,7 +764,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); - const useLock = bounds.r - bounds.x > 120; + const useLock = bounds.r - bounds.x > 135 && seldocview.props.CollectionFreeFormDocumentView; const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate? const rotation = NumCast(seldocview.rootDoc._rotation); @@ -795,6 +777,32 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); const radiusHandle = (borderRadius / docMax) * maxDist; const radiusHandleLocation = Math.min(radiusHandle, maxDist); + + const titleArea = this._editingTitle ? ( + !hideTitle && this.titleBlur()} + onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))} + onKeyDown={hideTitle ? emptyFunction : this.titleEntered} + onPointerDown={e => e.stopPropagation()} + /> + ) : ( +
+ {`${hideTitle ? '' : this.selectionTitle}`} + {!useLock ? null : ( + toggle ability to interact with document
} placement="top"> +
e.preventDefault()}> + +
+ + )} + + ); return (
e.preventDefault()} /> )} - {!useLock ? null : ( -
e.preventDefault()}> - -
- )} {hideDocumentButtonBar ? null : (
Date: Thu, 17 Nov 2022 14:35:13 -0500 Subject: mostly changing strings to enums --- src/client/util/DictationManager.ts | 6 +- src/client/util/LinkFollower.ts | 4 +- src/client/views/LightboxView.tsx | 16 +- src/client/views/MainView.tsx | 28 +- .../views/PropertiesDocBacklinksSelector.tsx | 45 ++- src/client/views/PropertiesDocContextSelector.tsx | 6 +- src/client/views/PropertiesView.tsx | 4 +- .../views/collections/CollectionDockingView.tsx | 26 +- .../views/collections/CollectionNoteTakingView.tsx | 12 +- .../views/collections/CollectionPileView.tsx | 22 +- .../views/collections/CollectionStackingView.tsx | 4 +- .../views/collections/CollectionTimeView.tsx | 351 +++++++++------- src/client/views/collections/CollectionView.tsx | 11 +- src/client/views/collections/TabDocView.tsx | 34 +- src/client/views/collections/TreeView.tsx | 8 +- .../CollectionFreeFormLayoutEngines.tsx | 4 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 23 +- .../CollectionMulticolumnView.tsx | 9 +- .../collectionSchema/CollectionSchemaCells.tsx | 447 +++++++++++---------- .../CollectionSchemaMovableRow.tsx | 3 +- .../collections/collectionSchema/SchemaTable.tsx | 6 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 4 +- src/client/views/nodes/DocumentView.tsx | 35 +- src/client/views/nodes/KeyValueBox.tsx | 5 +- src/client/views/nodes/KeyValuePair.tsx | 65 +-- src/client/views/nodes/LinkAnchorBox.tsx | 5 +- src/client/views/nodes/LinkDocPreview.tsx | 4 +- src/client/views/nodes/VideoBox.tsx | 3 +- src/client/views/nodes/button/FontIconBox.tsx | 3 +- .../views/nodes/formattedText/DashFieldView.tsx | 3 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 4 +- .../formattedText/ProsemirrorExampleTransfer.ts | 3 +- src/client/views/nodes/trails/PresBox.tsx | 13 +- src/mobile/MobileInterface.tsx | 2 +- 34 files changed, 668 insertions(+), 550 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 0a61f3478..203d4ad62 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -11,7 +11,7 @@ import { Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { DictationOverlay } from '../views/DictationOverlay'; -import { DocumentView } from '../views/nodes/DocumentView'; +import { DocumentView, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView'; import { SelectionManager } from './SelectionManager'; import { UndoManager } from './UndoManager'; @@ -328,7 +328,7 @@ export namespace DictationManager { { action: (target: DocumentView) => { const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 }); - target.props.addDocTab(kvp, 'add:right'); + target.props.addDocTab(kvp, OpenWhere.addRight); }, }, ], @@ -345,7 +345,7 @@ export namespace DictationManager { const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_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, 'add:right'); + target.props.addDocTab(newBox, OpenWhere.addRight); }, }, ], diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index 68716a207..a3eb7ed7a 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -4,7 +4,7 @@ import { BoolCast, Cast, DocCast, StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { DocumentDecorations } from '../views/DocumentDecorations'; import { LightboxView } from '../views/LightboxView'; -import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView'; +import { DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView'; import { DocumentManager } from './DocumentManager'; import { LinkManager } from './LinkManager'; import { UndoManager } from './UndoManager'; @@ -32,7 +32,7 @@ export class LinkFollower { const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => { const createTabForTarget = (didFocus: boolean) => new Promise(res => { - const where = LightboxView.LightboxDoc ? 'inPlace' : StrCast(sourceDoc.followLinkLocation, followLoc); + const where = LightboxView.LightboxDoc ? OpenWhere.inPlace : (StrCast(sourceDoc.followLinkLocation, followLoc) as OpenWhere); docViewProps.addDocTab(doc, where); setTimeout(() => { const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise. diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 5660a34e9..1f58763d1 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -18,7 +18,7 @@ import { TabDocView } from './collections/TabDocView'; import { GestureOverlay } from './GestureOverlay'; import './LightboxView.scss'; import { MainView } from './MainView'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider'; interface LightboxViewProps { @@ -141,11 +141,13 @@ export class LightboxView extends React.Component { this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`); } } - public static AddDocTab = (doc: Doc, location: string, layoutTemplate?: Doc, openInTabFunc?: any) => { - const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined; - if (inPlaceView) { - inPlaceView.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List([doc]); - return true; + public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc, openInTabFunc?: any) => { + if (location !== OpenWhere.lightbox) { + const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined; + if (inPlaceView) { + inPlaceView.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List([doc]); + return true; + } } LightboxView.openInTabFunc = openInTabFunc; SelectionManager.DeselectAll(); @@ -360,7 +362,7 @@ export class LightboxView extends React.Component { title={'open in tab'} onClick={e => { e.stopPropagation(); - CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, ''); + CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, OpenWhereMod.none); //LightboxView.openInTabFunc(LightboxView._docTarget || LightboxView._doc!, "inPlace"); SelectionManager.DeselectAll(); LightboxView.SetLightboxDoc(undefined); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 392b4eeeb..c151aebcd 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -49,7 +49,7 @@ import { LinkMenu } from './linking/LinkMenu'; import './MainView.scss'; import { AudioBox } from './nodes/AudioBox'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from './nodes/formattedText/RichTextMenu'; @@ -538,7 +538,7 @@ export class MainView extends React.Component { @action createNewPresentation = () => { const pres = Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true); - CollectionDockingView.AddSplit(pres, 'right'); + CollectionDockingView.AddSplit(pres, OpenWhereMod.right); Doc.MyTrails && Doc.AddDocToList(Doc.MyTrails, 'data', pres); // Doc.MyTrails should be created in createDashboard Doc.ActivePresentation = pres; }; @@ -546,7 +546,7 @@ export class MainView extends React.Component { @action openPresentation = (pres: Doc) => { if (pres.type === DocumentType.PRES) { - CollectionDockingView.AddSplit(pres, 'right'); + CollectionDockingView.AddSplit(pres, OpenWhereMod.right); Doc.MyTrails && (Doc.ActivePresentation = pres); Doc.AddDocToList(Doc.MyTrails, 'data', pres); this.closeFlyout(); @@ -683,20 +683,20 @@ export class MainView extends React.Component { sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1); mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0); - addDocTabFunc = (doc: Doc, location: string): boolean => { - const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':'); - const locationParams = locationFields.length > 1 ? locationFields[1] : ''; + addDocTabFunc = (doc: Doc, location: OpenWhere): boolean => { + const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':'); + const whereMods = whereFields.length > 1 ? whereFields[1] : ''; if (doc.dockingConfig) return DashboardView.openDashboard(doc); // prettier-ignore - switch (locationFields[0]) { + switch (whereFields[0]) { default: - case 'inPlace': - case 'lightbox': return LightboxView.AddDocTab(doc, location); - case 'add': return CollectionDockingView.AddSplit(doc, locationParams); - case 'dashboard': return DashboardView.openDashboard(doc); - case 'close': return CollectionDockingView.CloseSplit(doc, locationParams); - case 'fullScreen': return CollectionDockingView.OpenFullScreen(doc); - case 'toggle': return CollectionDockingView.ToggleSplit(doc, locationParams); + case OpenWhere.inPlace: + case OpenWhere.lightbox: return LightboxView.AddDocTab(doc, location); + case OpenWhere.add: return CollectionDockingView.AddSplit(doc, whereMods as OpenWhereMod); + case OpenWhere.dashboard: return DashboardView.openDashboard(doc); + case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods); + case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc); + case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods as OpenWhereMod); } }; diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx index 4ead8eaf0..25ac44078 100644 --- a/src/client/views/PropertiesDocBacklinksSelector.tsx +++ b/src/client/views/PropertiesDocBacklinksSelector.tsx @@ -1,20 +1,21 @@ -import { computed } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { Doc, DocListCast } from "../../fields/Doc"; -import { Cast } from "../../fields/Types"; -import { emptyFunction } from "../../Utils"; -import { DocumentType } from "../documents/DocumentTypes"; -import { LinkManager } from "../util/LinkManager"; -import { SelectionManager } from "../util/SelectionManager"; -import { LinkMenu } from "./linking/LinkMenu"; +import { computed } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, DocListCast } from '../../fields/Doc'; +import { Cast } from '../../fields/Types'; +import { emptyFunction } from '../../Utils'; +import { DocumentType } from '../documents/DocumentTypes'; +import { LinkManager } from '../util/LinkManager'; +import { SelectionManager } from '../util/SelectionManager'; +import { LinkMenu } from './linking/LinkMenu'; +import { OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import './PropertiesDocBacklinksSelector.scss'; type PropertiesDocBacklinksSelectorProps = { - Document: Doc, - Stack?: any, - hideTitle?: boolean, - addDocTab(doc: Doc, location: string): void + Document: Doc; + Stack?: any; + hideTitle?: boolean; + addDocTab(doc: Doc, location: OpenWhere): void; }; @observer @@ -40,14 +41,16 @@ export class PropertiesDocBacklinksSelector extends React.Component - {this.props.hideTitle ? (null) :

Contexts:

} - -
; + return !SelectionManager.Views().length ? null : ( +
+ {this.props.hideTitle ? null :

Contexts:

} + +
+ ); } -} \ No newline at end of file +} diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx index 9d89ee036..2c7da5931 100644 --- a/src/client/views/PropertiesDocContextSelector.tsx +++ b/src/client/views/PropertiesDocContextSelector.tsx @@ -7,14 +7,14 @@ import { Cast, NumCast, StrCast } from '../../fields/Types'; import { CollectionViewType } from '../documents/DocumentTypes'; import { DocFocusOrOpen } from '../util/DocumentManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import './PropertiesDocContextSelector.scss'; type PropertiesDocContextSelectorProps = { DocView?: DocumentView; Stack?: any; hideTitle?: boolean; - addDocTab(doc: Doc, location: string): void; + addDocTab(doc: Doc, location: OpenWhere): void; }; @observer @@ -53,7 +53,7 @@ export class PropertiesDocContextSelector extends React.Component DocFocusOrOpen(Doc.GetProto(this.props.DocView!.props.Document), col), 100); }; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index e5ff9e267..93a3fd253 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -24,7 +24,7 @@ import { Transform } from '../util/Transform'; import { undoBatch, UndoManager } from '../util/UndoManager'; import { EditableView } from './EditableView'; import { InkStrokeProperties } from './InkStrokeProperties'; -import { DocumentView, StyleProviderFunc } from './nodes/DocumentView'; +import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView'; import { FilterBox } from './nodes/FilterBox'; import { KeyValueBox } from './nodes/KeyValueBox'; import { PresBox } from './nodes/trails'; @@ -42,7 +42,7 @@ interface PropertiesViewProps { width: number; height: number; styleProvider?: StyleProviderFunc; - addDocTab: (doc: Doc, where: string) => boolean; + addDocTab: (doc: Doc, where: OpenWhere) => boolean; } @observer diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 434466505..8cbe548c7 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -25,6 +25,7 @@ import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { TabDocView } from './TabDocView'; import React = require('react'); +import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -142,7 +143,7 @@ export class CollectionDockingView extends CollectionSubView() { @undoBatch @action - public static ReplaceTab(document: Doc, panelName: string, stack: any, addToSplit?: boolean): boolean { + public static ReplaceTab(document: Doc, panelName: OpenWhereMod, stack: any, addToSplit?: boolean): boolean { const instance = CollectionDockingView.Instance; if (!instance) return false; const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName); @@ -164,7 +165,7 @@ export class CollectionDockingView extends CollectionSubView() { } @undoBatch - public static ToggleSplit(doc: Doc, location: string, stack?: any, panelName?: string) { + public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string) { return CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1 ? CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName); @@ -175,7 +176,7 @@ export class CollectionDockingView extends CollectionSubView() { // @undoBatch @action - public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) { + public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string) { if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document); if (!CollectionDockingView.Instance) return false; const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document); @@ -208,14 +209,15 @@ export class CollectionDockingView extends CollectionSubView() { // if row switch (pullSide) { default: - case 'right': + case OpenWhereMod.none: + case OpenWhereMod.right: glayRoot.contentItems[0].addChild(newContentItem()); break; - case 'left': + case OpenWhereMod.left: glayRoot.contentItems[0].addChild(newContentItem(), 0); break; - case 'top': - case 'bottom': + case OpenWhereMod.top: + case OpenWhereMod.bottom: // if not going in a row layout, must add already existing content into column const rowlayout = glayRoot.contentItems[0]; const newColumn = rowlayout.layoutManager.createContentItem({ type: 'column' }, instance._goldenLayout); @@ -496,7 +498,7 @@ export class CollectionDockingView extends CollectionSubView() { title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); - CollectionDockingView.AddSplit(docToAdd, '', stack); + CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }); @@ -539,7 +541,7 @@ export class CollectionDockingView extends CollectionSubView() { title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); - CollectionDockingView.AddSplit(docToAdd, '', stack); + CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }) ); @@ -568,14 +570,14 @@ export class CollectionDockingView extends CollectionSubView() { ScriptingGlobals.add( function openInLightbox(doc: any) { - LightboxView.AddDocTab(doc, 'lightbox'); + LightboxView.AddDocTab(doc, OpenWhere.lightbox); }, 'opens up document in a lightbox', '(doc: any)' ); ScriptingGlobals.add( function openOnRight(doc: any) { - return CollectionDockingView.AddSplit(doc, 'right'); + return CollectionDockingView.AddSplit(doc, OpenWhereMod.right); }, 'opens up document in tab on right side of the screen', '(doc: any)' @@ -588,5 +590,5 @@ ScriptingGlobals.add( '(doc: any)' ); ScriptingGlobals.add(function useRightSplit(doc: any, shiftKey?: boolean) { - CollectionDockingView.ReplaceTab(doc, 'right', undefined, shiftKey); + CollectionDockingView.ReplaceTab(doc, OpenWhereMod.right, undefined, shiftKey); }); diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index b0f64ed60..29670a1a7 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -19,7 +19,7 @@ import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { LightboxView } from '../LightboxView'; -import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView'; +import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { StyleProp } from '../StyleProvider'; @@ -180,14 +180,6 @@ export class CollectionNoteTakingView extends CollectionSubView() { return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } - addDocTab = (doc: Doc, where: string) => { - if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { - this.dataDoc[this.props.fieldKey] = new List([doc]); - return true; - } - return this.props.addDocTab(doc, where); - }; - scrollToBottom = () => { smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight); }; @@ -274,7 +266,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { removeDocument={this.props.removeDocument} contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.addDocTab} + addDocTab={this.props.addDocTab} bringToFront={returnFalse} scriptContext={this.props.scriptContext} pinToPres={this.props.pinToPres} diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 38e240ac6..e95622630 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -12,6 +12,8 @@ import './CollectionPileView.scss'; import { CollectionSubView } from './CollectionSubView'; import React = require('react'); import { ScriptField } from '../../../fields/ScriptField'; +import { OpenWhere } from '../nodes/DocumentView'; +import { computePassLayout, computeStarburstLayout } from './collectionFreeForm'; @observer export class CollectionPileView extends CollectionSubView() { @@ -19,8 +21,8 @@ export class CollectionPileView extends CollectionSubView() { _disposers: { [name: string]: IReactionDisposer } = {}; componentDidMount() { - if (this.layoutEngine() !== 'pass' && this.layoutEngine() !== 'starburst') { - this.Document._pileLayoutEngine = 'pass'; + if (this.layoutEngine() !== computePassLayout.name && this.layoutEngine() !== computeStarburstLayout.name) { + this.Document._pileLayoutEngine = computePassLayout.name; } this._originalChrome = this.layoutDoc._chromeHidden; this.layoutDoc._chromeHidden = true; @@ -56,7 +58,7 @@ export class CollectionPileView extends CollectionSubView() { // returns the contents of the pileup in a CollectionFreeFormView @computed get contents() { - const isStarburst = this.layoutEngine() === 'starburst'; + const isStarburst = this.layoutEngine() === computeStarburstLayout.name; return (
{ - if (this.layoutEngine() === 'starburst') { + if (this.layoutEngine() === computeStarburstLayout.name) { const defaultSize = 110; this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2; this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2; @@ -83,12 +85,12 @@ export class CollectionPileView extends CollectionSubView() { DocUtils.pileup(this.childDocs, undefined, undefined, NumCast(this.layoutDoc._width) / 2, false); this.layoutDoc._panX = 0; this.layoutDoc._panY = -10; - this.props.Document._pileLayoutEngine = 'pass'; + this.props.Document._pileLayoutEngine = computePassLayout.name; } else { const defaultSize = 25; !this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 250); !this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5); - if (this.layoutEngine() === 'pass') { + if (this.layoutEngine() === computePassLayout.name) { this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2; this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - defaultSize / 2; this.layoutDoc._starburstPileWidth = this.layoutDoc[WidthSym](); @@ -96,7 +98,7 @@ export class CollectionPileView extends CollectionSubView() { } this.layoutDoc._panX = this.layoutDoc._panY = 0; this.layoutDoc._width = this.layoutDoc._height = defaultSize; - this.props.Document._pileLayoutEngine = 'starburst'; + this.props.Document._pileLayoutEngine = computeStarburstLayout.name; } }); @@ -118,7 +120,7 @@ export class CollectionPileView extends CollectionSubView() { const doc = this.childDocs[0]; doc.x = e.clientX; doc.y = e.clientY; - this.props.addDocTab(doc, 'inParent') && (this.props.removeDocument?.(doc) || false); + this.props.addDocTab(doc, OpenWhere.inParent) && (this.props.removeDocument?.(doc) || false); dist = 0; } } @@ -130,8 +132,8 @@ export class CollectionPileView extends CollectionSubView() { SnappingManager.SetIsDragging(false); }, emptyFunction, - e.shiftKey && this.layoutEngine() === 'pass', - this.layoutEngine() === 'pass' && e.shiftKey + e.shiftKey && this.layoutEngine() === computePassLayout.name, + this.layoutEngine() === computePassLayout.name && e.shiftKey ); // this sets _doubleTap }; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 175051d5c..aa4583af6 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -22,7 +22,7 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from '../EditableView'; import { LightboxView } from '../LightboxView'; import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; -import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView'; +import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { StyleProp } from '../StyleProvider'; @@ -241,7 +241,7 @@ export class CollectionStackingView extends CollectionSubView this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } - addDocTab = (doc: Doc, where: string) => { + addDocTab = (doc: Doc, where: OpenWhere) => { if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { this.dataDoc[this.props.fieldKey] = new List([doc]); return true; diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 3dd9d2d84..ac896a8fd 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -1,32 +1,32 @@ -import { toUpper } from "lodash"; -import { action, computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, Opt, StrListCast } from "../../../fields/Doc"; -import { List } from "../../../fields/List"; -import { ObjectField } from "../../../fields/ObjectField"; -import { RichTextField } from "../../../fields/RichTextField"; -import { listSpec } from "../../../fields/Schema"; -import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils"; -import { Docs } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { DocumentManager } from "../../util/DocumentManager"; -import { ScriptingGlobals } from "../../util/ScriptingGlobals"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { EditableView } from "../EditableView"; -import { ViewSpecPrefix } from "../nodes/DocumentView"; -import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngines"; -import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; -import { CollectionSubView } from "./CollectionSubView"; -import "./CollectionTimeView.scss"; -import React = require("react"); +import { toUpper } from 'lodash'; +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, Opt, StrListCast } from '../../../fields/Doc'; +import { List } from '../../../fields/List'; +import { ObjectField } from '../../../fields/ObjectField'; +import { RichTextField } from '../../../fields/RichTextField'; +import { listSpec } from '../../../fields/Schema'; +import { ComputedField, ScriptField } from '../../../fields/ScriptField'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DocumentManager } from '../../util/DocumentManager'; +import { ScriptingGlobals } from '../../util/ScriptingGlobals'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { EditableView } from '../EditableView'; +import { ViewSpecPrefix } from '../nodes/DocumentView'; +import { computePivotLayout, computeTimelineLayout, ViewDefBounds } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { CollectionSubView } from './CollectionSubView'; +import './CollectionTimeView.scss'; +import React = require('react'); @observer export class CollectionTimeView extends CollectionSubView() { _changing = false; - @observable _layoutEngine = "pivot"; + @observable _layoutEngine = computePivotLayout.name; @observable _collapsed: boolean = false; @observable _childClickedScript: Opt; @observable _viewDefDivClick: Opt; @@ -35,7 +35,7 @@ export class CollectionTimeView extends CollectionSubView() { getAnchor = () => { const anchor = Docs.Create.HTMLAnchorDocument([], { title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any, - annotationOn: this.rootDoc + annotationOn: this.rootDoc, }); // save view spec information for anchor @@ -43,81 +43,103 @@ export class CollectionTimeView extends CollectionSubView() { proto.pivotField = this.pivotField; proto.docFilters = ObjectField.MakeCopy(this.layoutDoc._docFilters as ObjectField) || new List([]); proto.docRangeFilters = ObjectField.MakeCopy(this.layoutDoc._docRangeFilters as ObjectField) || new List([]); - proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType; + proto[ViewSpecPrefix + '_viewType'] = this.layoutDoc._viewType; // store anchor in annotations list of document (not technically needed since these anchors are never drawn) - if (Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), null) !== undefined) { - Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), []).push(anchor); + if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) { + Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor); } else { - this.dataDoc[this.props.fieldKey + "-annotations"] = new List([anchor]); + this.dataDoc[this.props.fieldKey + '-annotations'] = new List([anchor]); } return anchor; - } + }; async componentDidMount() { this.props.setContentView?.(this); //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), ""); ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List(['dropAction']); useRightSplit(alias, shiftKey); "; runInAction(() => { - this._childClickedScript = ScriptField.MakeScript("openInLightbox(self)", { this: Doc.name }); - this._viewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" }); + this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name }); + this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' }); }); } - get pivotField() { return this._focusPivotField || StrCast(this.layoutDoc._pivotField); } + get pivotField() { + return this._focusPivotField || StrCast(this.layoutDoc._pivotField); + } @action setViewSpec = (anchor: Doc, preview: boolean) => { - if (preview) { // if in preview, then override document's fields with view spec + if (preview) { + // if in preview, then override document's fields with view spec this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters); this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters); this._focusPivotField = StrCast(anchor.pivotField); - } else if (anchor.pivotField !== undefined) { // otherwise set document's fields based on anchor view spec + } else if (anchor.pivotField !== undefined) { + // otherwise set document's fields based on anchor view spec this.layoutDoc._prevFilterIndex = 1; this.layoutDoc._pivotField = StrCast(anchor.pivotField); this.layoutDoc._docFilters = new List(StrListCast(anchor.docFilters)); this.layoutDoc._docRangeFilters = new List(StrListCast(anchor.docRangeFilters)); } return 0; - } + }; layoutEngine = () => this._layoutEngine; - toggleVisibility = action(() => this._collapsed = !this._collapsed); + toggleVisibility = action(() => (this._collapsed = !this._collapsed)); onMinDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); - const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); - this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth(); - this.props.Document[this.props.fieldKey + "-timelineSpan"] = undefined; - return false; - }), returnFalse, emptyFunction); - } + setupMoveUpEvents( + this, + e, + action((e: PointerEvent, down: number[], delta: number[]) => { + const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0)); + const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10)); + this.props.Document[this.props.fieldKey + '-timelineMinReq'] = minReq + ((maxReq - minReq) * delta[0]) / this.props.PanelWidth(); + this.props.Document[this.props.fieldKey + '-timelineSpan'] = undefined; + return false; + }), + returnFalse, + emptyFunction + ); + }; onMaxDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); - const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); - this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth(); - return false; - }), returnFalse, emptyFunction); - } + setupMoveUpEvents( + this, + e, + action((e: PointerEvent, down: number[], delta: number[]) => { + const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0)); + const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10)); + this.props.Document[this.props.fieldKey + '-timelineMaxReq'] = maxReq + ((maxReq - minReq) * delta[0]) / this.props.PanelWidth(); + return false; + }), + returnFalse, + emptyFunction + ); + }; onMidDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); - const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); - this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth(); - this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth(); - return false; - }), returnFalse, emptyFunction); - } + setupMoveUpEvents( + this, + e, + action((e: PointerEvent, down: number[], delta: number[]) => { + const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0)); + const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10)); + this.props.Document[this.props.fieldKey + '-timelineMinReq'] = minReq - ((maxReq - minReq) * delta[0]) / this.props.PanelWidth(); + this.props.Document[this.props.fieldKey + '-timelineMaxReq'] = maxReq - ((maxReq - minReq) * delta[0]) / this.props.PanelWidth(); + return false; + }), + returnFalse, + emptyFunction + ); + }; goTo = (prevFilterIndex: number) => { - this.layoutDoc._pivotField = this.layoutDoc["_prevPivotFields" + prevFilterIndex]; - this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc["_prevDocFilter" + prevFilterIndex] as ObjectField); - this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc["_prevDocRangeFilters" + prevFilterIndex] as ObjectField); + this.layoutDoc._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex]; + this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField); + this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField); this.layoutDoc._prevFilterIndex = prevFilterIndex; - } + }; @action contentsDown = (e: React.MouseEvent) => { @@ -127,37 +149,58 @@ export class CollectionTimeView extends CollectionSubView() { } else { this.layoutDoc._docFilters = new List([]); } - } + }; dontScaleFilter = (doc: Doc) => doc.type === DocumentType.RTF; @computed get contents() { - return
- -
; + return ( +
+ +
+ ); } public static SyncTimelineToPresentation(doc: Doc) { const fieldKey = Doc.LayoutFieldKey(doc); - doc[fieldKey + "-timelineCur"] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)"); + doc[fieldKey + '-timelineCur'] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)"); } specificMenu = (e: React.MouseEvent) => { const layoutItems: ContextMenuProps[] = []; const doc = this.layoutDoc; - layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline"; }, icon: "compress-arrows-alt" }); - layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot"; }, icon: "compress-arrows-alt" }); - layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined; }, icon: "compress-arrows-alt" }); - layoutItems.push({ description: "Sync with presentation", event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: "compress-arrows-alt" }); + layoutItems.push({ + description: 'Force Timeline', + event: () => { + doc._forceRenderEngine = computeTimelineLayout.name; + }, + icon: 'compress-arrows-alt', + }); + layoutItems.push({ + description: 'Force Pivot', + event: () => { + doc._forceRenderEngine = computePivotLayout.name; + }, + icon: 'compress-arrows-alt', + }); + layoutItems.push({ + description: 'Auto Time/Pivot layout', + event: () => { + doc._forceRenderEngine = undefined; + }, + icon: 'compress-arrows-alt', + }); + layoutItems.push({ description: 'Sync with presentation', event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: 'compress-arrows-alt' }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" }); - } + ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' }); + }; @computed get _allFacets() { const facets = new Set(); this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); @@ -169,37 +212,40 @@ export class CollectionTimeView extends CollectionSubView() { const docItems: ContextMenuProps[] = []; const keySet: Set = new Set(); - this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey => - pair.layout[fieldKey] instanceof RichTextField || - typeof (pair.layout[fieldKey]) === "number" || - typeof (pair.layout[fieldKey]) === "boolean" || - typeof (pair.layout[fieldKey]) === "string").filter(fieldKey => fieldKey[0] !== "_" && (fieldKey[0] !== "#" || fieldKey === "#") && (fieldKey === "tags" || fieldKey[0] === toUpper(fieldKey)[0])).map(fieldKey => keySet.add(fieldKey))); - Array.from(keySet).map(fieldKey => - docItems.push({ description: ":" + fieldKey, event: () => this.layoutDoc._pivotField = fieldKey, icon: "compress-arrows-alt" })); - docItems.push({ description: ":default", event: () => this.layoutDoc._pivotField = undefined, icon: "compress-arrows-alt" }); - ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" }); + this.childLayoutPairs.map(pair => + this._allFacets + .filter(fieldKey => pair.layout[fieldKey] instanceof RichTextField || typeof pair.layout[fieldKey] === 'number' || typeof pair.layout[fieldKey] === 'boolean' || typeof pair.layout[fieldKey] === 'string') + .filter(fieldKey => fieldKey[0] !== '_' && (fieldKey[0] !== '#' || fieldKey === '#') && (fieldKey === 'tags' || fieldKey[0] === toUpper(fieldKey)[0])) + .map(fieldKey => keySet.add(fieldKey)) + ); + Array.from(keySet).map(fieldKey => docItems.push({ description: ':' + fieldKey, event: () => (this.layoutDoc._pivotField = fieldKey), icon: 'compress-arrows-alt' })); + docItems.push({ description: ':default', event: () => (this.layoutDoc._pivotField = undefined), icon: 'compress-arrows-alt' }); + ContextMenu.Instance.addItem({ description: 'Pivot Fields ...', subitems: docItems, icon: 'eye' }); const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y); - ContextMenu.Instance.displayMenu(x, y, ":"); - } + ContextMenu.Instance.displayMenu(x, y, ':'); + }; @computed get pivotKeyUI() { - return
- { - if (value?.length) { - this.layoutDoc._pivotField = value; - return true; - } - return false; - }} - toggle={this.toggleVisibility} - background={"#f1efeb"} // this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; - contents={":" + StrCast(this.layoutDoc._pivotField)} - showMenuOnLoad={true} - display={"inline"} - menuCallback={this.menuCallback} /> -
; + return ( +
+ { + if (value?.length) { + this.layoutDoc._pivotField = value; + return true; + } + return false; + }} + toggle={this.toggleVisibility} + background={'#f1efeb'} // this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + contents={':' + StrCast(this.layoutDoc._pivotField)} + showMenuOnLoad={true} + display={'inline'} + menuCallback={this.menuCallback} + /> +
+ ); } render() { @@ -211,55 +257,62 @@ export class CollectionTimeView extends CollectionSubView() { } }); const forceLayout = StrCast(this.layoutDoc._forceRenderEngine); - const doTimeline = forceLayout ? (forceLayout === "timeline") : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6; - if (doTimeline !== (this._layoutEngine === "timeline")) { + const doTimeline = forceLayout ? forceLayout === computeTimelineLayout.name : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6; + if (doTimeline !== (this._layoutEngine === computeTimelineLayout.name)) { if (!this._changing) { this._changing = true; - setTimeout(action(() => { - this._layoutEngine = doTimeline ? "timeline" : "pivot"; - this._changing = false; - }), 0); + setTimeout( + action(() => { + this._layoutEngine = doTimeline ? computeTimelineLayout.name : computePivotLayout.name; + this._changing = false; + }), + 0 + ); } } - return
- {this.pivotKeyUI} - {this.contents} - {!this.props.isSelected() || !doTimeline ? (null) : <> -
-
-
- } -
; + return ( +
+ {this.pivotKeyUI} + {this.contents} + {!this.props.isSelected() || !doTimeline ? null : ( + <> +
+
+
+ + )} +
+ ); } } ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) { - const pivotField = StrCast(pivotDoc._pivotField) || "author"; + const pivotField = StrCast(pivotDoc._pivotField) || 'author'; let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex); const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField)); - pivotDoc["_prevDocFilter" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField); - pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField); - pivotDoc["_prevPivotFields" + prevFilterIndex] = pivotField; + pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField); + pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField); + pivotDoc['_prevPivotFields' + prevFilterIndex] = pivotField; pivotDoc._prevFilterIndex = ++prevFilterIndex; pivotDoc._docFilters = new List(); - setTimeout(action(() => { - const filterVals = (bounds.payload as string[]); - filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, "check")); - const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc); - if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) { - if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) { - pivotDoc._pivotField = filterVals[0]; + setTimeout( + action(() => { + const filterVals = bounds.payload as string[]; + filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, 'check')); + const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc); + if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) { + if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) { + pivotDoc._pivotField = filterVals[0]; + } } - } - const newFilters = StrListCast(pivotDoc._docFilters); - if (newFilters.length && originalFilter.length && - newFilters.lastElement() === originalFilter.lastElement()) { - pivotDoc._prevFilterIndex = --prevFilterIndex; - pivotDoc["_prevDocFilter" + prevFilterIndex] = undefined; - pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = undefined; - pivotDoc["_prevPivotFields" + prevFilterIndex] = undefined; - } - })); -}); \ No newline at end of file + const newFilters = StrListCast(pivotDoc._docFilters); + if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) { + pivotDoc._prevFilterIndex = --prevFilterIndex; + pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined; + pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = undefined; + pivotDoc['_prevPivotFields' + prevFilterIndex] = undefined; + } + }) + ); +}); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 625d4e9e5..917d7618c 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -16,6 +16,7 @@ import { InteractionUtils } from '../../util/InteractionUtils'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { CollectionCarousel3DView } from './CollectionCarousel3DView'; import { CollectionCarouselView } from './CollectionCarouselView'; @@ -174,7 +175,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent { const newRendition = Doc.MakeAlias(this.rootDoc); newRendition._viewType = vtype; - this.props.addDocTab(newRendition, 'add:right'); + this.props.addDocTab(newRendition, OpenWhere.addRight); return newRendition; }, false @@ -184,17 +185,17 @@ export class CollectionView extends ViewBoxAnnotatableComponent (this.rootDoc.forceActive = !this.rootDoc.forceActive), icon: 'project-diagram' }) : null; if (this.rootDoc.childLayout instanceof Doc) { - optionItems.push({ description: 'View Child Layout', event: () => this.props.addDocTab(this.rootDoc.childLayout as Doc, 'add:right'), icon: 'project-diagram' }); + optionItems.push({ description: 'View Child Layout', event: () => this.props.addDocTab(this.rootDoc.childLayout as Doc, OpenWhere.addRight), icon: 'project-diagram' }); } if (this.rootDoc.childClickedOpenTemplateView instanceof Doc) { - optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, 'add:right'), icon: 'project-diagram' }); + optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, OpenWhere.addRight), icon: 'project-diagram' }); } !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? 'Unset' : 'Set'} inPlace Container`, event: () => (this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer), icon: 'project-diagram' }); if (!Doc.noviceMode && false) { optionItems.push({ description: 'Create Branch', - event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), 'add:right'), + event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight), icon: 'project-diagram', }); optionItems.push({ @@ -225,7 +226,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent { const alias = Doc.MakeAlias(this.rootDoc); DocUtils.makeCustomViewClicked(alias, undefined, func.key); - this.props.addDocTab(alias, 'add:right'); + this.props.addDocTab(alias, OpenWhere.addRight); }, }) ); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index e21649648..2cc588b78 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -26,7 +26,7 @@ import { DashboardView } from '../DashboardView'; import { Colors, Shadows } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; import { MainView } from '../MainView'; -import { DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; import { DashFieldView } from '../nodes/formattedText/DashFieldView'; import { PinProps, PresBox, PresMovement } from '../nodes/trails'; import { DefaultStyleProvider, StyleProp } from '../StyleProvider'; @@ -296,7 +296,7 @@ export class TabDocView extends React.Component { ) { const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []); if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1); - CollectionDockingView.AddSplit(curPres, 'right'); + CollectionDockingView.AddSplit(curPres, OpenWhereMod.right); setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things } setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs @@ -343,34 +343,30 @@ export class TabDocView extends React.Component { // "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name, // "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right // inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack - addDocTab = (doc: Doc, location: string) => { + addDocTab = (doc: Doc, location: OpenWhere) => { SelectionManager.DeselectAll(); - const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':'); - const locationParams = locationFields.length > 1 ? locationFields[1] : ''; + const locationFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':'); + const locationParams: OpenWhereMod = locationFields.length > 1 ? (locationFields[1] as OpenWhereMod) : OpenWhereMod.none; switch (locationFields[0]) { - case 'dashboard': + case OpenWhere.dashboard: return DashboardView.openDashboard(doc); - case 'close': + case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, locationParams); - case 'fullScreen': + case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc); - case 'replace': + case OpenWhere.replace: return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack); - // case "lightbox": { - // // TabDocView.PinDoc(doc, { hidePresBox: true }); - // return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab); - // } - case 'inPlace': + case OpenWhere.inPlace: const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined; if (inPlaceView) { inPlaceView.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List([doc]); return true; - } - case 'lightbox': + } // fall through to lightbox + case OpenWhere.lightbox: return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab); - case 'toggle': + case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, locationParams, this.stack); - case 'add': + case OpenWhere.add: default: return CollectionDockingView.AddSplit(doc, locationParams, this.stack); } @@ -509,7 +505,7 @@ interface TabMinimapViewProps { document: Doc; hideMinimap: () => boolean; tabView: () => DocumentView | undefined; - addDocTab: (doc: Doc, where: string) => boolean; + addDocTab: (doc: Doc, where: OpenWhere) => boolean; PanelWidth: () => number; PanelHeight: () => number; background: () => string; diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 13cf64558..bd326f917 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -22,7 +22,7 @@ import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { EditableView } from '../EditableView'; import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss'; -import { DocumentView, DocumentViewInternal, DocumentViewProps, StyleProviderFunc } from '../nodes/DocumentView'; +import { DocumentView, DocumentViewInternal, DocumentViewProps, OpenWhere, StyleProviderFunc } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; @@ -44,7 +44,7 @@ export interface TreeViewProps { containerCollection: Doc; renderDepth: number; dropAction: dropActionType; - addDocTab: (doc: Doc, where: string) => boolean; + addDocTab: (doc: Doc, where: OpenWhere) => boolean; panelWidth: () => number; panelHeight: () => number; addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; @@ -236,7 +236,7 @@ export class TreeView extends React.Component { const bestAlias = docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsPrototype(docView.props.Document) ? docView.props.Document : DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail); const nextBestAlias = DocListCast(this.props.document.aliases).find(doc => doc.author === Doc.CurrentUserEmail); - this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), 'lightbox'); + this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), OpenWhere.lightbox); } }; @@ -1109,7 +1109,7 @@ export class TreeView extends React.Component { remove: undefined | ((doc: Doc | Doc[]) => boolean), move: DragManager.MoveFunction, dropAction: dropActionType, - addDocTab: (doc: Doc, where: string) => boolean, + addDocTab: (doc: Doc, where: OpenWhere) => boolean, styleProvider: undefined | StyleProviderFunc, screenToLocalXf: () => Transform, isContentActive: (outsideReaction?: boolean) => boolean, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 54be6ba0f..7dd9cdb8b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -82,7 +82,7 @@ interface PivotColumn { filters: string[]; } -export function computerPassLayout(poolData: Map, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { +export function computePassLayout(poolData: Map, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { const docMap = new Map(); childPairs.forEach(({ layout, data }, i) => { docMap.set(layout[Id], { @@ -97,7 +97,7 @@ export function computerPassLayout(poolData: Map, pivotDoc: Do return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []); } -export function computerStarburstLayout(poolData: Map, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { +export function computeStarburstLayout(poolData: Map, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { const mustFit = pivotDoc[WidthSym]() !== panelDim[0]; // if a panel size is set that's not the same as the pivot doc's size, then assume this is in a panel for a content fitting view (like a grid) in which case everything must be scaled to stay within the panel const docMap = new Map(); const docSize = mustFit ? panelDim[0] * 0.33 : 75; // assume an icon sized at 75 diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 57cccec4a..8cabf060d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -37,7 +37,7 @@ import { GestureOverlay } from '../../GestureOverlay'; import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; import { LightboxView } from '../../LightboxView'; import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView'; -import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView'; +import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { PresBox } from '../../nodes/trails/PresBox'; @@ -47,7 +47,7 @@ import { StyleProp } from '../../StyleProvider'; import { CollectionSubView } from '../CollectionSubView'; import { TreeViewType } from '../CollectionTreeView'; import { TabDocView } from '../TabDocView'; -import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines'; +import { computePivotLayout, computePassLayout as computePassLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines'; import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors'; import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; @@ -1272,7 +1272,8 @@ export class CollectionFreeFormView extends CollectionSubView { const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); - const pointerEvents = this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === 'pass' && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.()); + const pointerEvents = + this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.()); return pointerEvents; }; getChildDocView(entry: PoolData) { @@ -1328,8 +1329,8 @@ export class CollectionFreeFormView extends CollectionSubView ); } - addDocTab = action((doc: Doc, where: string) => { - if (where === 'inParent') { + addDocTab = action((doc: Doc, where: OpenWhere) => { + if (where === OpenWhere.inParent) { (doc instanceof Doc ? [doc] : doc).forEach(doc => { const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = pt[0]; @@ -1337,7 +1338,7 @@ export class CollectionFreeFormView extends CollectionSubView(doc as any as Doc[]); return true; } @@ -1457,10 +1458,10 @@ export class CollectionFreeFormView extends CollectionSubView(); // prettier-ignore switch (this.layoutEngine) { - case 'pass': return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) }; - case 'timeline': return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) }; - case 'pivot': return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) }; - case 'starburst': return { newPool, computedElementData: this.doEngineLayout(newPool, computerStarburstLayout) }; + case computePassLayout.name : return { newPool, computedElementData: this.doEngineLayout(newPool, computePassLayout) }; + case computeTimelineLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) }; + case computePivotLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) }; + case computeStarburstLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeStarburstLayout) }; } return { newPool, computedElementData: this.doFreeformLayout(newPool) }; } @@ -1724,7 +1725,7 @@ export class CollectionFreeFormView extends CollectionSubView ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); - addDocTab = (doc: Doc, where: string) => { - if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { - this.dataDoc[this.props.fieldKey] = new List([doc]); - return true; - } - return this.props.addDocTab(doc, where); - }; focusDocument = (doc: Doc, options: DocFocusOptions) => this.props.focus(this.rootDoc, options); isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive(); isChildContentActive = () => (((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.isContentActive() ? true : false); @@ -278,7 +271,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.addDocTab} + addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} bringToFront={returnFalse} /> diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx index adcd9e1e3..ef75fb159 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx @@ -1,35 +1,36 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import { extname } from "path"; -import DatePicker from "react-datepicker"; -import { CellInfo } from "react-table"; -import { DateField } from "../../../../fields/DateField"; -import { Doc, DocListCast, Field, Opt } from "../../../../fields/Doc"; -import { Id } from "../../../../fields/FieldSymbols"; -import { List } from "../../../../fields/List"; -import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; -import { ComputedField } from "../../../../fields/ScriptField"; -import { BoolCast, Cast, DateCast, FieldValue, StrCast } from "../../../../fields/Types"; -import { ImageField } from "../../../../fields/URLField"; -import { emptyFunction, Utils } from "../../../../Utils"; -import { Docs } from "../../../documents/Documents"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { DocumentManager } from "../../../util/DocumentManager"; -import { DragManager } from "../../../util/DragManager"; -import { KeyCodes } from "../../../util/KeyCodes"; -import { CompileScript } from "../../../util/Scripting"; -import { SearchUtil } from "../../../util/SearchUtil"; -import { SnappingManager } from "../../../util/SnappingManager"; -import { undoBatch } from "../../../util/UndoManager"; +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { extname } from 'path'; +import DatePicker from 'react-datepicker'; +import { CellInfo } from 'react-table'; +import { DateField } from '../../../../fields/DateField'; +import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +import { List } from '../../../../fields/List'; +import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; +import { ComputedField } from '../../../../fields/ScriptField'; +import { BoolCast, Cast, DateCast, FieldValue, StrCast } from '../../../../fields/Types'; +import { ImageField } from '../../../../fields/URLField'; +import { emptyFunction, Utils } from '../../../../Utils'; +import { Docs } from '../../../documents/Documents'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { DocumentManager } from '../../../util/DocumentManager'; +import { DragManager } from '../../../util/DragManager'; +import { KeyCodes } from '../../../util/KeyCodes'; +import { CompileScript } from '../../../util/Scripting'; +import { SearchUtil } from '../../../util/SearchUtil'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { undoBatch } from '../../../util/UndoManager'; import '../../../views/DocumentDecorations.scss'; -import { EditableView } from "../../EditableView"; +import { EditableView } from '../../EditableView'; import { MAX_ROW_HEIGHT } from '../../global/globalCssVariables.scss'; -import { DocumentIconContainer } from "../../nodes/DocumentIcon"; -import { OverlayView } from "../../OverlayView"; -import { CollectionView } from "../CollectionView"; -import "./CollectionSchemaView.scss"; +import { DocumentIconContainer } from '../../nodes/DocumentIcon'; +import { OverlayView } from '../../OverlayView'; +import { CollectionView } from '../CollectionView'; +import './CollectionSchemaView.scss'; +import { OpenWhere } from '../../nodes/DocumentView'; // intialize cell properties export interface CellProps { @@ -46,10 +47,9 @@ export interface CellProps { // currently unused renderDepth: number; // called when a button is pressed on the node itself - addDocTab: (document: Doc, where: string) => boolean; + addDocTab: (document: Doc, where: OpenWhere) => boolean; pinToPres: (document: Doc) => void; - moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, - addDocument: (document: Doc | Doc[]) => boolean) => boolean; + moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; isFocused: boolean; changeFocusedCellByIndex: (row: number, col: number) => void; // set whether the cell is in the isEditing mode @@ -67,7 +67,7 @@ export class CollectionSchemaCell extends React.Component { // return a field key that is corrected for whether it COMMENT public static resolvedFieldKey(column: string, rowDoc: Doc) { const fieldKey = column; - if (fieldKey.startsWith("*")) { + if (fieldKey.startsWith('*')) { const rootKey = fieldKey.substring(1); const allKeys = [...Array.from(Object.keys(rowDoc)), ...Array.from(Object.keys(Doc.GetProto(rowDoc)))]; const matchedKeys = allKeys.filter(key => key.includes(rootKey)); @@ -82,33 +82,37 @@ export class CollectionSchemaCell extends React.Component { protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original); // methods for dragging and dropping protected _dropDisposer?: DragManager.DragDropDisposer; - @observable contents: string = ""; + @observable contents: string = ''; - componentDidMount() { document.addEventListener("keydown", this.onKeyDown); } - componentWillUnmount() { document.removeEventListener("keydown", this.onKeyDown); } + componentDidMount() { + document.addEventListener('keydown', this.onKeyDown); + } + componentWillUnmount() { + document.removeEventListener('keydown', this.onKeyDown); + } @action onKeyDown = (e: KeyboardEvent): void => { // If a cell is editable and clicked, hitting enter shoudl allow the user to edit it if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) { - document.removeEventListener("keydown", this.onKeyDown); + document.removeEventListener('keydown', this.onKeyDown); this._isEditing = true; this.props.setIsEditing(true); } - } + }; @action isEditingCallback = (isEditing: boolean): void => { // a general method that takes a boolean that determines whether the cell should be in // is-editing mode // remove the event listener if it's there - document.removeEventListener("keydown", this.onKeyDown); + document.removeEventListener('keydown', this.onKeyDown); // it's not already in is-editing mode, re-add the event listener - isEditing && document.addEventListener("keydown", this.onKeyDown); + isEditing && document.addEventListener('keydown', this.onKeyDown); this._isEditing = isEditing; this.props.setIsEditing(isEditing); this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - } + }; @action onPointerDown = async (e: React.PointerEvent): Promise => { @@ -119,19 +123,19 @@ export class CollectionSchemaCell extends React.Component { this.props.setPreviewDoc(this.props.rowProps.original); let url: string; - if (url = StrCast(this.props.rowProps.row.href)) { + if ((url = StrCast(this.props.rowProps.row.href))) { // opens up the the doc in a new window, blurring the old one try { new URL(url); const temp = window.open(url)!; temp.blur(); window.focus(); - } catch { } + } catch {} } const doc = Cast(this._rowDoc[this.renderFieldKey], Doc, null); doc && this.props.setPreviewDoc(doc); - } + }; @undoBatch applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => { @@ -142,7 +146,7 @@ export class CollectionSchemaCell extends React.Component { doc[this.renderFieldKey] = res.result; return true; // return whether the change was successful - } + }; private drop = (e: Event, de: DragManager.DropEvent) => { // if the drag has data at its completion @@ -151,41 +155,51 @@ export class CollectionSchemaCell extends React.Component { if (de.complete.docDragData.draggedDocuments.length === 1) { // update the renderFieldKey this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0]; - } - else { + } else { // create schema document reflecting the new column arrangement - const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {}); + const coll = Docs.Create.SchemaDocument([new SchemaHeaderField('title', '#f1efeb')], de.complete.docDragData.draggedDocuments, {}); this._rowDataDoc[this.renderFieldKey] = coll; } e.stopPropagation(); } - } + }; protected dropRef = (ele: HTMLElement | null) => { // if the drop disposer is not undefined, run its function this._dropDisposer?.(); // if ele is not null, give ele a non-undefined drop disposer ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this))); - } + }; returnHighlights(contents: string, positions?: number[]) { if (positions) { const results = []; StrCast(this.props.Document._searchString); const length = StrCast(this.props.Document._searchString).length; - const color = contents ? "black" : "grey"; + const color = contents ? 'black' : 'grey'; - results.push({contents?.slice(0, positions[0])}); + results.push( + + {contents?.slice(0, positions[0])} + + ); positions.forEach((num, cur) => { - results.push({contents?.slice(num, num + length)}); + results.push( + + {contents?.slice(num, num + length)} + + ); let end = 0; - cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1]; - results.push({contents?.slice(num + length, end)}); - } - ); + cur === positions.length - 1 ? (end = contents.length) : (end = positions[cur + 1]); + results.push( + + {contents?.slice(num + length, end)} + + ); + }); return results; } - return {contents ? contents?.valueOf() : "undefined"}; + return {contents ? contents?.valueOf() : 'undefined'}; } @computed get renderFieldKey() { @@ -199,10 +213,9 @@ export class CollectionSchemaCell extends React.Component { const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc); const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null); // Jump to the this document - DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [], - undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc)); + DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [], undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc)); } - } + }; renderCellWithType(type: string | undefined) { const dragRef: React.RefObject = React.createRef(); @@ -214,29 +227,29 @@ export class CollectionSchemaCell extends React.Component { const onPointerEnter = (e: React.PointerEvent): void => { // e.buttons === 1 means the left moue pointer is down - if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) { - dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over"; + if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === 'document' || type === undefined)) { + dragRef.current!.className = 'collectionSchemaView-cellContainer doc-drag-over'; } }; const onPointerLeave = (e: React.PointerEvent): void => { // change the class name to indicate that the cell is no longer being dragged - dragRef.current!.className = "collectionSchemaView-cellContainer"; + dragRef.current!.className = 'collectionSchemaView-cellContainer'; }; let contents = Field.toString(field as Field); // display 2 hyphens instead of a blank box for empty cells - contents = contents === "" ? "--" : contents; + contents = contents === '' ? '--' : contents; // classname reflects the tatus of the cell - let className = "collectionSchemaView-cellWrapper"; - if (this._isEditing) className += " editing"; - if (this.props.isFocused && this.props.isEditable) className += " focused"; - if (this.props.isFocused && !this.props.isEditable) className += " inactive"; + let className = 'collectionSchemaView-cellWrapper'; + if (this._isEditing) className += ' editing'; + if (this.props.isFocused && this.props.isEditable) className += ' focused'; + if (this.props.isFocused && !this.props.isEditable) className += ' inactive'; const positions = []; - if (StrCast(this.props.Document._searchString).toLowerCase() !== "") { + if (StrCast(this.props.Document._searchString).toLowerCase() !== '') { // term is ...promise pending... if the field is a Promise, otherwise it is the cell's contents - let term = (field instanceof Promise) ? "...promise pending..." : contents.toLowerCase(); + let term = field instanceof Promise ? '...promise pending...' : contents.toLowerCase(); const search = StrCast(this.props.Document._searchString).toLowerCase(); let start = term.indexOf(search); let tally = 0; @@ -256,56 +269,60 @@ export class CollectionSchemaCell extends React.Component { positions.pop(); } } - const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined"; + const placeholder = type === 'number' ? '0' : contents === '' ? '--' : 'undefined'; return ( -
this._isEditing = true)} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> +
(this._isEditing = true))} + onPointerEnter={onPointerEnter} + onPointerLeave={onPointerLeave}>
-
- {!this.props.Document._searchDoc ? +
+ {!this.props.Document._searchDoc ? ( { const cfield = ComputedField.WithoutComputed(() => FieldValue(field)); const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined; - const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1]; - return cscript ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : - Field.IsField(cfield) ? Field.toScriptString(cfield) : ""; + const cfinalScript = cscript?.split('return')[cscript.split('return').length - 1]; + return cscript ? (cfinalScript?.endsWith(';') ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : Field.IsField(cfield) ? Field.toScriptString(cfield) : ''; }} SetValue={action((value: string) => { // sets what is displayed after the user makes an input let retVal = false; - if (value.startsWith(":=") || value.startsWith("=:=")) { + if (value.startsWith(':=') || value.startsWith('=:=')) { // decides how to compute a value when given either of the above strings - const script = value.substring(value.startsWith("=:=") ? 3 : 2); - retVal = this.props.setComputed(script, value.startsWith(":=") ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col); + const script = value.substring(value.startsWith('=:=') ? 3 : 2); + retVal = this.props.setComputed(script, value.startsWith(':=') ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col); } else { // check if the input is a number let inputIsNum = true; for (const s of value) { - if (isNaN(parseInt(s)) && !(s === ".") && !(s === ",")) { + if (isNaN(parseInt(s)) && !(s === '.') && !(s === ',')) { inputIsNum = false; } } // check if the input is a boolean - const inputIsBool: boolean = value === "false" || value === "true"; - // what to do in the case - if (!inputIsNum && !inputIsBool && !value.startsWith("=")) { + const inputIsBool: boolean = value === 'false' || value === 'true'; + // what to do in the case + if (!inputIsNum && !inputIsBool && !value.startsWith('=')) { // if it's not a number, it's a string, and should be processed as such - // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically + // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically // after each edit let valueSansQuotes = value; if (this._isEditing) { const vsqLength = valueSansQuotes.length; // get rid of outer quotes - valueSansQuotes = valueSansQuotes.substring(value.startsWith("\"") ? 1 : 0, - valueSansQuotes.charAt(vsqLength - 1) === "\"" ? vsqLength - 1 : vsqLength); + valueSansQuotes = valueSansQuotes.substring(value.startsWith('"') ? 1 : 0, valueSansQuotes.charAt(vsqLength - 1) === '"' ? vsqLength - 1 : vsqLength); } let inputAsString = '"'; // escape any quotes in the string @@ -319,27 +336,27 @@ export class CollectionSchemaCell extends React.Component { // add a closing quote inputAsString += '"'; //two options here: we can strip off outer quotes or we can figure out what's going on with the script - const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); const changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length; // change it if a change is made, otherwise, just compile using the old cell conetnts script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); // handle numbers and expressions - } else if (inputIsNum || value.startsWith("=")) { + } else if (inputIsNum || value.startsWith('=')) { //TODO: make accept numbers - const inputscript = value.substring(value.startsWith("=") ? 1 : 0); + const inputscript = value.substring(value.startsWith('=') ? 1 : 0); // if commas are not stripped, the parser only considers the numbers after the last comma - let inputSansCommas = ""; + let inputSansCommas = ''; for (const s of inputscript) { - if (!(s === ",")) { + if (!(s === ',')) { inputSansCommas += s; } } - const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); const changeMade = value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); // handle booleans } else if (inputIsBool) { - const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); const changeMade = value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); } @@ -352,33 +369,47 @@ export class CollectionSchemaCell extends React.Component { })} OnFillDown={async (value: string) => { // computes all of the value preceded by := - const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - script.compiled && DocListCast(this.props.Document[this.props.fieldKey]). - forEach((doc, i) => value.startsWith(":=") ? - this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) : - this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run)); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); + script.compiled && + DocListCast(this.props.Document[this.props.fieldKey]).forEach((doc, i) => + value.startsWith(':=') ? this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) : this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run) + ); }} /> - : + ) : ( this.returnHighlights(contents, positions) - } -
+ )} +
); } - render() { return this.renderCellWithType(undefined); } + render() { + return this.renderCellWithType(undefined); + } } @observer -export class CollectionSchemaNumberCell extends CollectionSchemaCell { render() { return this.renderCellWithType("number"); } } +export class CollectionSchemaNumberCell extends CollectionSchemaCell { + render() { + return this.renderCellWithType('number'); + } +} @observer -export class CollectionSchemaBooleanCell extends CollectionSchemaCell { render() { return this.renderCellWithType("boolean"); } } +export class CollectionSchemaBooleanCell extends CollectionSchemaCell { + render() { + return this.renderCellWithType('boolean'); + } +} @observer -export class CollectionSchemaStringCell extends CollectionSchemaCell { render() { return this.renderCellWithType("string"); } } +export class CollectionSchemaStringCell extends CollectionSchemaCell { + render() { + return this.renderCellWithType('string'); + } +} @observer export class CollectionSchemaDateCell extends CollectionSchemaCell { @@ -396,24 +427,24 @@ export class CollectionSchemaDateCell extends CollectionSchemaCell { // ^ DateCast is always undefined for some reason, but that is what the field should be set to this._rowDoc[this.renderFieldKey] = new DateField(date as Date); //} - } + }; render() { - return !this.props.isFocused ? {this._date ? Field.toString(this._date as Field) : "--"} : - this.handleChange(date)} - onChange={date => this.handleChange(date)} - />; + return !this.props.isFocused ? ( + {this._date ? Field.toString(this._date as Field) : '--'} + ) : ( + this.handleChange(date)} onChange={date => this.handleChange(date)} /> + ); } } @observer export class CollectionSchemaDocCell extends CollectionSchemaCell { - _overlayDisposer?: () => void; - @computed get _doc() { return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); } + @computed get _doc() { + return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); + } @action onSetValue = (value: string) => { @@ -422,7 +453,7 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { const script = CompileScript(value, { addReturn: true, typecheck: true, - transformer: DocumentIconContainer.getTransformer() + transformer: DocumentIconContainer.getTransformer(), }); // compile the script const results = script.compiled && script.run(); @@ -432,44 +463,43 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { return true; } return false; - } + }; - componentWillUnmount() { this.onBlur(); } + componentWillUnmount() { + this.onBlur(); + } - onBlur = () => { this._overlayDisposer?.(); }; + onBlur = () => { + this._overlayDisposer?.(); + }; onFocus = () => { this.onBlur(); this._overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); - } + }; @action isEditingCallback = (isEditing: boolean): void => { // the isEditingCallback from a general CollectionSchemaCell - document.removeEventListener("keydown", this.onKeyDown); - isEditing && document.addEventListener("keydown", this.onKeyDown); + document.removeEventListener('keydown', this.onKeyDown); + isEditing && document.addEventListener('keydown', this.onKeyDown); this._isEditing = isEditing; this.props.setIsEditing(isEditing); this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - } + }; render() { // if there's a doc, render it - return !this._doc ? this.renderCellWithType("document") : -
-
+ return !this._doc ? ( + this.renderCellWithType('document') + ) : ( +
+
StrCast(this._doc?.title)} SetValue={action((value: string) => { @@ -477,33 +507,36 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { return true; })} /> -
-
this._doc && this.props.addDocTab(this._doc, "add:right")} className="collectionSchemaView-cellContents-docButton"> +
+
this._doc && this.props.addDocTab(this._doc, OpenWhere.addRight)} className="collectionSchemaView-cellContents-docButton">
-
; +
+ ); } } @observer export class CollectionSchemaImageCell extends CollectionSchemaCell { - choosePath(url: URL) { - if (url.protocol === "data") return url.href; // if the url ises the data protocol, just return the href + if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here — good question + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question const ext = extname(url.href); - return url.href.replace(ext, "_o" + ext); + return url.href.replace(ext, '_o' + ext); } render() { const field = Cast(this._rowDoc[this.renderFieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc - const alts = DocListCast(this._rowDoc[this.renderFieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images - const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents + const alts = DocListCast(this._rowDoc[this.renderFieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images + const altpaths = alts + .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url) + .filter(url => url) + .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; // If there is a path, follow it; otherwise, follow a link to a default image icon - const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; + const url = paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')]; const aspect = Doc.NativeAspect(this._rowDoc); // aspect ratio let width = Math.min(75, this.props.rowProps.width); // get a with that is no smaller than 75px @@ -511,25 +544,28 @@ export class CollectionSchemaImageCell extends CollectionSchemaCell { width = height * aspect; // increase the width of the image if necessary to maintain proportionality const reference = React.createRef(); - return
-
- -
-
; + return ( +
+
+ +
+
+ ); } } - @observer export class CollectionSchemaListCell extends CollectionSchemaCell { _overlayDisposer?: () => void; - @computed get _field() { return this._rowDoc[this.renderFieldKey]; } - @computed get _optionsList() { return this._field as List; } + @computed get _field() { + return this._rowDoc[this.renderFieldKey]; + } + @computed get _optionsList() { + return this._field as List; + } @observable private _opened = false; // whether the list is opened - @observable private _text = "select an item"; + @observable private _text = 'select an item'; @observable private _selectedNum = 0; // the index of the list item selected @action @@ -538,102 +574,109 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { this._optionsList[this._selectedNum] = this._text = value; (this._field as List).splice(this._selectedNum, 1, value); - } + }; @action onSelected = (element: string, index: number) => { // if an item is selected, the private variables should update to reflect this this._text = element; this._selectedNum = index; - } + }; onFocus = () => { this._overlayDisposer?.(); this._overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); - } + }; render() { const link = false; const reference = React.createRef(); - // if the list is not opened, don't display it; otherwise, do. + // if the list is not opened, don't display it; otherwise, do. if (this._optionsList?.length) { - const options = !this._opened ? (null) : + const options = !this._opened ? null : (
{this._optionsList.map((element, index) => { const val = Field.toString(element); - return
this.onSelected(StrCast(element), index)} > - {val} -
; + return ( +
this.onSelected(StrCast(element), index)}> + {val} +
+ ); })} -
; - - const plainText =
{this._text}
; - const textarea =
- this._text} - SetValue={action((value: string) => { - // add special for params - this.onSetValue(value); - return true; - })} - /> -
; +
+ ); + + const plainText =
{this._text}
; + const textarea = ( +
+ this._text} + SetValue={action((value: string) => { + // add special for params + this.onSetValue(value); + return true; + })} + /> +
+ ); //☰ return (
-
{link ? plainText : textarea}
{options} -
+
); } - return this.renderCellWithType("list"); + return this.renderCellWithType('list'); } } - @observer export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { - @computed get _isChecked() { return BoolCast(this._rowDoc[this.renderFieldKey]); } + @computed get _isChecked() { + return BoolCast(this._rowDoc[this.renderFieldKey]); + } render() { const reference = React.createRef(); return (
- this._rowDoc[this.renderFieldKey] = e.target.checked} /> + (this._rowDoc[this.renderFieldKey] = e.target.checked)} />
); } } - @observer export class CollectionSchemaButtons extends CollectionSchemaCell { // the navigation buttons for schema view when it is used for search. render() { - return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <> : -
+ return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? ( + <> + ) : ( +
- -
; +
+ ); } } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx index f872637e5..3cb2df7d3 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx @@ -10,6 +10,7 @@ import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; +import { OpenWhere } from '../../nodes/DocumentView'; import './CollectionSchemaView.scss'; export interface MovableRowProps { @@ -138,7 +139,7 @@ export class MovableRow extends React.Component
-
this.props.addDocTab(this.props.rowInfo.original, 'add:right')}> +
this.props.addDocTab(this.props.rowInfo.original, OpenWhere.addRight)}>
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx index fafea5ce3..45ad4f86b 100644 --- a/src/client/views/collections/collectionSchema/SchemaTable.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx @@ -23,7 +23,7 @@ import { undoBatch } from '../../../util/UndoManager'; import '../../../views/DocumentDecorations.scss'; import { ContextMenu } from '../../ContextMenu'; import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss'; -import { DocumentView } from '../../nodes/DocumentView'; +import { DocumentView, OpenWhere } from '../../nodes/DocumentView'; import { DefaultStyleProvider } from '../../StyleProvider'; import { CollectionView } from '../CollectionView'; import { @@ -86,7 +86,7 @@ export interface SchemaTableProps { ScreenToLocalTransform: () => Transform; active: (outsideReaction: boolean | undefined) => boolean | undefined; onDrop: (e: React.DragEvent, options: DocumentOptions, completed?: (() => void) | undefined) => void; - addDocTab: (document: Doc, where: string) => boolean; + addDocTab: (document: Doc, where: OpenWhere) => boolean; pinToPres: (document: Doc) => void; isSelected: (outsideReaction?: boolean) => boolean; isFocused: (document: Doc, outsideReaction: boolean) => boolean; @@ -625,7 +625,7 @@ export class SchemaTable extends React.Component { }; onOpenClick = () => { - this._showDoc && this.props.addDocTab(this._showDoc, 'add:right'); + this._showDoc && this.props.addDocTab(this._showDoc, OpenWhere.addRight); }; getPreviewTransform = (): Transform => { diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 570039550..868822fbf 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -16,7 +16,7 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec import { DocComponent } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import './CollectionFreeFormDocumentView.scss'; -import { DocumentView, DocumentViewProps } from './DocumentView'; +import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import React = require('react'); export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { @@ -199,7 +199,7 @@ export class CollectionFreeFormDocumentView extends DocComponent string; whenChildContentsActiveChanged: (isActive: boolean) => void; rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected - addDocTab: (doc: Doc, where: string) => boolean; + addDocTab: (doc: Doc, where: OpenWhere) => boolean; filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) addDocument?: (doc: Doc | Doc[]) => boolean; removeDocument?: (doc: Doc | Doc[]) => boolean; @@ -474,7 +495,7 @@ export class DocumentViewInternal extends DocComponent this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "map-pin", selected: -1 }); + // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: "map-pin", selected: -1 }); const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && RadialMenu.Instance.addItem({ @@ -485,7 +506,7 @@ export class DocumentViewInternal extends DocComponent this.props.addDocTab(this.props.Document, "add:right"), icon: "trash", selected: -1 }); + // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, OpenWhere.addRight), icon: "trash", selected: -1 }); RadialMenu.Instance.addItem({ description: 'Pin', event: () => this.props.pinToPres(this.props.Document, {}), icon: 'map-pin', selected: -1 }); RadialMenu.Instance.addItem({ description: 'Open', event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: 'trash', selected: -1 }); @@ -586,7 +607,7 @@ export class DocumentViewInternal extends DocComponent (func().result?.select === true ? this.props.select(false) : ''), 'on double click'); } else if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isLinkButton) { - UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, 'lightbox', this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap'); + UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap'); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } @@ -857,7 +878,7 @@ export class DocumentViewInternal extends DocComponent this.props.addDocTab(templateDoc, 'add:right'), icon: 'eye' }); + !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); !Doc.noviceMode && appearanceItems.push({ description: 'Add a Field', @@ -957,8 +978,8 @@ export class DocumentViewInternal extends DocComponent this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), 'add:right'), icon: 'layer-group' }); - !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), 'add:right'), icon: 'keyboard' }); + helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' }); + !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' }); !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' }); !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' }); diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 7d04c4b64..18c5b81ec 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -20,6 +20,7 @@ import { ContextMenuProps } from '../ContextMenuItem'; import e = require('express'); import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { ImageBox } from './ImageBox'; +import { OpenWhere } from './DocumentView'; export type KVPScript = { script: CompiledScript; @@ -259,8 +260,8 @@ export class KeyValueBox extends React.Component { openItems.push({ description: 'Default Perspective', event: () => { - this.props.addDocTab(this.props.Document, 'close'); - this.props.addDocTab(this.fieldDocToLayout, 'add:right'); + this.props.addDocTab(this.props.Document, OpenWhere.close); + this.props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight); }, icon: 'image', }); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 80def3025..e74ef4a39 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,18 +1,19 @@ import { action, observable } from 'mobx'; -import { observer } from "mobx-react"; +import { observer } from 'mobx-react'; import { Doc, Field, Opt } from '../../../fields/Doc'; import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter, returnEmptyDoclist, emptyPath } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; -import { EditableView } from "../EditableView"; +import { EditableView } from '../EditableView'; import { FieldView, FieldViewProps } from './FieldView'; import { KeyValueBox } from './KeyValueBox'; -import "./KeyValueBox.scss"; -import "./KeyValuePair.scss"; -import React = require("react"); +import './KeyValueBox.scss'; +import './KeyValuePair.scss'; +import React = require('react'); import { DefaultStyleProvider } from '../StyleProvider'; +import { OpenWhere } from './DocumentView'; // Represents one row in a key value plane @@ -23,7 +24,7 @@ export interface KeyValuePairProps { keyWidth: number; PanelHeight: () => number; PanelWidth: () => number; - addDocTab: (doc: Doc, where: string) => boolean; + addDocTab: (doc: Doc, where: OpenWhere) => boolean; } @observer export class KeyValuePair extends React.Component { @@ -34,23 +35,23 @@ export class KeyValuePair extends React.Component { @action handleCheck = (e: React.ChangeEvent) => { this.isChecked = e.currentTarget.checked; - } + }; @action uncheck = () => { this.checkbox.current!.checked = false; this.isChecked = false; - } + }; onContextMenu = (e: React.MouseEvent) => { const value = this.props.doc[this.props.keyName]; if (value instanceof Doc) { e.stopPropagation(); e.preventDefault(); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } - } + }; render() { const props: FieldViewProps = { @@ -68,7 +69,7 @@ export class KeyValuePair extends React.Component { isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, - dropAction: "alias", + dropAction: 'alias', bringToFront: emptyFunction, renderDepth: 1, isContentActive: returnFalse, @@ -92,30 +93,30 @@ export class KeyValuePair extends React.Component { doc = doc.proto; } const parenCount = Math.max(0, protoCount - 1); - const keyStyle = protoCount === 0 ? "black" : "blue"; + const keyStyle = protoCount === 0 ? 'black' : 'blue'; - const hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; + const hover = { transition: '0.3s ease opacity', opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; return ( - this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}> + (this.isPointerOver = true))} onPointerLeave={action(() => (this.isPointerOver = false))}>
- - -
{"(".repeat(parenCount)}{props.fieldKey}{")".repeat(parenCount)}
+ +
+ {'('.repeat(parenCount)} + {props.fieldKey} + {')'.repeat(parenCount)} +
@@ -123,13 +124,13 @@ export class KeyValuePair extends React.Component { Field.toKeyValueString(props.Document, props.fieldKey)} - SetValue={(value: string) => - KeyValueBox.SetField(props.Document, props.fieldKey, value)} /> + SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} + />
); } -} \ No newline at end of file +} diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index d6cf79f87..be9565452 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -17,6 +17,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import './LinkAnchorBox.scss'; import { LinkDocPreview } from './LinkDocPreview'; import React = require('react'); +import { OpenWhere } from './DocumentView'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -88,13 +89,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent() { }; openLinkDocOnRight = (e: React.MouseEvent) => { - this.props.addDocTab(this.rootDoc, 'add:right'); + this.props.addDocTab(this.rootDoc, OpenWhere.addRight); }; openLinkTargetOnRight = (e: React.MouseEvent) => { const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null)); alias._isLinkButton = undefined; alias.layoutKey = 'layout'; - this.props.addDocTab(alias, 'add:right'); + this.props.addDocTab(alias, OpenWhere.addRight); }; @action openLinkEditor = action((e: React.MouseEvent) => { diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index a47577701..135fbca31 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -14,7 +14,7 @@ import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; -import { DocumentView, DocumentViewSharedProps } from './DocumentView'; +import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView'; import './LinkDocPreview.scss'; import React = require('react'); import { LinkEditor } from '../linking/LinkEditor'; @@ -156,7 +156,7 @@ export class LinkDocPreview extends React.Component { LinkDocPreview.Clear(); LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false); } else if (this.props.hrefs?.length) { - this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), 'add:right'); + this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), OpenWhere.addRight); } }; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 70ac84fa4..82d5b00f9 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -31,6 +31,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import './VideoBox.scss'; import { ObjectField } from '../../../fields/ObjectField'; +import { OpenWhere } from './DocumentView'; const path = require('path'); /** @@ -273,7 +274,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent() { } showTemplate = (): void => { const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); - dragFactory && this.props.addDocTab(dragFactory, 'add:right'); + dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight); }; dragAsTemplate = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 1e7cb6ea5..63347015b 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -17,6 +17,7 @@ import { Tooltip } from '@material-ui/core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { NodeSelection } from 'prosemirror-state'; +import { OpenWhere } from '../DocumentView'; export class DashFieldView { dom: HTMLDivElement; // container for label and value @@ -227,7 +228,7 @@ export class DashFieldViewInternal extends React.Component c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, '#f1efeb')); list.map(c => c.heading).indexOf('text') === -1 && list.push(new SchemaHeaderField('text', '#f1efeb')); alias._pivotField = this._fieldKey.startsWith('#') ? '#' : this._fieldKey; - this.props.tbox.props.addDocTab(alias, 'add:right'); + this.props.tbox.props.addDocTab(alias, OpenWhere.addRight); } }; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index fdd61463d..ce4639b76 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -45,7 +45,7 @@ import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; -import { DocumentViewInternal } from '../DocumentView'; +import { DocumentViewInternal, OpenWhere } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { LinkDocPreview } from '../LinkDocPreview'; import { DashDocCommentView } from './DashDocCommentView'; @@ -1428,7 +1428,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const docView = DocumentManager.Instance.getDocumentView(audiodoc); if (!docView) { - this.props.addDocTab(audiodoc, 'add:bottom'); + this.props.addDocTab(audiodoc, OpenWhere.addBottom); setTimeout(func); } else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, 'number', null)); // bcz: would be nice to find the next audio tag in the doc and play until that }; diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 3d9bd6add..68b0488a2 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -9,6 +9,7 @@ import { GetEffectiveAcl } from '../../../../fields/util'; import { Utils } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { SelectionManager } from '../../../util/SelectionManager'; +import { OpenWhere } from '../DocumentView'; import { liftListItem, sinkListItem } from './prosemirrorPatches.js'; const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false; @@ -135,7 +136,7 @@ export function buildKeymap>(schema: S, props: any, mapKey //Command to create a new Tab with a PDF of all the command shortcuts bind('Mod-/', (state: EditorState, dispatch: (tx: Transaction) => void) => { const newDoc = Docs.Create.PdfDocument(Utils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 }); - props.addDocTab(newDoc, 'add:right'); + props.addDocTab(newDoc, OpenWhere.addRight); }); //Commands to modify BlockType diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index e19b53f50..adfd2fda1 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -23,7 +23,7 @@ import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; -import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/collectionFreeForm'; +import { CollectionFreeFormView, computeTimelineLayout, MarqueeViewBounds } from '../../collections/collectionFreeForm'; import { CollectionView } from '../../collections/CollectionView'; import { TabDocView } from '../../collections/TabDocView'; import { ViewBoxBaseComponent } from '../../DocComponent'; @@ -35,6 +35,7 @@ import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import { PresEffect, PresMovement, PresStatus } from './PresEnums'; import { map } from 'bluebird'; +import { OpenWhere, OpenWhereMod } from '../DocumentView'; const { Howl } = require('howler'); export interface PinProps { @@ -196,7 +197,7 @@ export class PresBox extends ViewBoxBaseComponent() { ); this.props.setContentView?.(this); this._unmounting = false; - this.rootDoc._forceRenderEngine = 'timeline'; + this.rootDoc._forceRenderEngine = computeTimelineLayout.name; this.layoutDoc.presStatus = PresStatus.Edit; this.layoutDoc._gridGap = 0; this.layoutDoc._yMargin = 0; @@ -563,7 +564,7 @@ export class PresBox extends ViewBoxBaseComponent() { self._eleArray.splice(0, self._eleArray.length, ...eleViewCache); }); const openInTab = (doc: Doc, finished?: () => void) => { - (collectionDocView ?? this).props.addDocTab(doc, ''); + (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.add); this.layoutDoc.presCollection = targetDoc; // this still needs some fixing setTimeout(resetSelection, 500); @@ -725,7 +726,7 @@ export class PresBox extends ViewBoxBaseComponent() { if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { this.layoutDoc.presStatus = PresStatus.Edit; Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc); - CollectionDockingView.AddSplit(this.rootDoc, 'right'); + CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right); } else { this.layoutDoc.presStatus = PresStatus.Edit; clearTimeout(this._presTimer); @@ -1760,7 +1761,7 @@ export class PresBox extends ViewBoxBaseComponent() { TabDocView.PinDoc(doc, {}); this.gotoDocument(this.childDocs.length, this.activeItem); } else { - this.props.addDocTab(doc, 'add:right'); + this.props.addDocTab(doc, OpenWhere.addRight); } } }; @@ -2322,7 +2323,7 @@ export class PresBox extends ViewBoxBaseComponent() { static NavigateToDoc(bestTarget: Doc, activeItem: Doc) { const srcContext = Cast(bestTarget.context, Doc, null) ?? Cast(Cast(bestTarget.annotationOn, Doc, null)?.context, Doc, null); const openInTab = (doc: Doc, finished?: () => void) => { - CollectionDockingView.AddSplit(doc, 'right'); + CollectionDockingView.AddSplit(doc, OpenWhereMod.right); finished?.(); }; PresBox.NavigateToTarget(bestTarget, activeItem, openInTab, srcContext); diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index 8265de445..2ae597b0b 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -700,7 +700,7 @@ export class MobileInterface extends React.Component { className="docButton" title={Doc.isDocPinned(this._activeDoc) ? 'Unpin from presentation' : 'Pin to presentation'} style={{ backgroundColor: isPinned ? 'black' : 'white', color: isPinned ? 'white' : 'black' }} - onClick={e => TabDocView.PinDoc(this._activeDoc)}> + onClick={e => TabDocView.PinDoc(this._activeDoc, {})}>
); -- cgit v1.2.3-70-g09d2 From 66184a172006de4d4bf72d9da33858e04d298181 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 1 Dec 2022 10:13:03 -0500 Subject: refactored process of following links / jumping to docs and added following options for zoomTime, etc instead of setting temporary fields on docs. --- src/client/documents/Documents.ts | 8 +- src/client/util/DocumentManager.ts | 98 +++--- .../util/Import & Export/DirectoryImportBox.tsx | 334 +++++++++++---------- src/client/util/LinkFollower.ts | 84 +++--- src/client/util/SharingManager.tsx | 2 +- src/client/views/DocComponent.tsx | 2 +- src/client/views/InkingStroke.tsx | 6 +- src/client/views/MainView.tsx | 2 +- src/client/views/MarqueeAnnotator.tsx | 2 +- src/client/views/PropertiesView.tsx | 28 +- .../views/collections/CollectionNoteTakingView.tsx | 5 +- .../views/collections/CollectionStackingView.tsx | 5 +- src/client/views/collections/TabDocView.tsx | 4 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 8 +- .../collections/collectionFreeForm/MarqueeView.tsx | 2 +- .../collectionLinear/CollectionLinearView.tsx | 2 +- .../collectionSchema/CollectionSchemaCells.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 26 +- src/client/views/nodes/ImageBox.tsx | 8 +- src/client/views/nodes/PDFBox.tsx | 13 +- src/client/views/nodes/VideoBox.tsx | 6 +- src/client/views/nodes/WebBox.tsx | 10 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 12 +- src/client/views/nodes/trails/PresBox.tsx | 24 +- src/client/views/pdf/Annotation.tsx | 4 +- src/client/views/pdf/PDFViewer.tsx | 8 +- src/client/views/search/SearchBox.tsx | 2 +- 27 files changed, 354 insertions(+), 353 deletions(-) (limited to 'src/client/views/nodes/VideoBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index eed839520..d13d96dd3 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -157,7 +157,7 @@ export class DocumentOptions { _contentBounds?: List; // the (forced) bounds of the document to display. format is: [left, top, right, bottom] _lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged _lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed - _isPushpin?: boolean; // whether document, when clicked, toggles display of its link target + _followLinkToggle?: boolean; // whether document, when clicked, toggles display of its link target _showTitle?: string; // field name to display in header (:hover is an optional suffix) _showCaption?: string; // which field to display in the caption area. leave empty to have no caption _scrollTop?: number; // scroll location for pdfs @@ -272,7 +272,7 @@ export class DocumentOptions { clipWidth?: number; // percent transition from before to after in comparisonBox dockingConfig?: string; annotationOn?: Doc; - isPushpin?: boolean; + followLinkToggle?: boolean; isGroup?: boolean; // whether a collection should use a grouping UI behavior _removeDropProperties?: List; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document noteType?: string; @@ -1703,7 +1703,7 @@ export namespace DocUtils { } export function LeavePushpin(doc: Doc, annotationField: string) { - if (doc.isPushpin) return undefined; + if (doc.followLinkToggle) return undefined; const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null); const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context)); if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { @@ -1711,7 +1711,7 @@ export namespace DocUtils { title: 'pushpin', label: '', annotationOn: Cast(doc.annotationOn, Doc, null), - isPushpin: true, + followLinkToggle: true, icon: 'map-pin', x: Cast(doc.x, 'number', null), y: Cast(doc.y, 'number', null), diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 4f02a8202..1b63b615b 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -5,7 +5,7 @@ import { Cast, DocCast } from '../../fields/Types'; import { returnFalse } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { LightboxView } from '../views/LightboxView'; -import { DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView'; +import { DocFocusOptions, DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView'; import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; @@ -171,19 +171,13 @@ export class DocumentManager { }; public jumpToDocument = ( targetDoc: Doc, // document to display - willZoom: boolean, // whether to zoom doc to take up most of screen + options: DocFocusOptions, // options for how to navigate to target createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist docContext: Doc[], // context to load that should contain the target - linkDoc?: Doc, // link that's being followed - closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there - originatingDoc: Opt = undefined, // doc that initiated the display of the target odoc - finished?: () => void, - originalTarget?: Doc, - noSelect?: boolean, - presZoomScale?: number + finished?: () => void ): void => { - originalTarget = originalTarget ?? targetDoc; - const docView = this.getFirstDocumentView(targetDoc, originatingDoc); + const originalTarget = options.originalTarget ?? targetDoc; + const docView = this.getFirstDocumentView(targetDoc, options.originatingDoc); const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null); const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? docView?.rootDoc ?? targetDoc : docView?.rootDoc ?? targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field var wasHidden = resolvedTarget.hidden; @@ -195,14 +189,14 @@ export class DocumentManager { } const focusAndFinish = (didFocus: boolean) => { const finalTargetDoc = resolvedTarget; - if (originatingDoc?.isPushpin) { + if (options.toggleTarget) { if (!didFocus && !wasHidden) { // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal finalTargetDoc.hidden = !finalTargetDoc.hidden; } } else { finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined); - !noSelect && docView?.select(false); + !options.noSelect && docView?.select(false); } finished?.(); }; @@ -216,9 +210,8 @@ export class DocumentManager { if (annoContainerView.props.Document.layoutKey === 'layout_icon') { annoContainerView.iconify(() => annoContainerView.focus(targetDoc, { + ...options, originalTarget, - willZoom, - scale: presZoomScale, afterFocus: (didFocus: boolean) => new Promise(res => { focusAndFinish(true); @@ -232,13 +225,12 @@ export class DocumentManager { } } if (focusView) { - !noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox - if (originatingDoc?.followLinkAudio) DocumentManager.playAudioAnno(focusView.rootDoc); + !options.noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox + if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc); const doFocus = (forceDidFocus: boolean) => - focusView.focus(originalTarget ?? targetDoc, { + focusView.focus(originalTarget, { + ...options, originalTarget, - willZoom, - scale: presZoomScale, afterFocus: (didFocus: boolean) => new Promise(res => { focusAndFinish(forceDidFocus || didFocus); @@ -262,13 +254,12 @@ export class DocumentManager { targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden targetDocContext._viewTransition = 'transform 500ms'; targetDocContextView.props.focus(targetDocContextView.rootDoc, { - willZoom, + ...options, + // originalTarget, // needed? afterFocus: async () => { targetDocContext._viewTransition = undefined; if (targetDocContext.layoutKey === 'layout_icon') { - targetDocContextView.iconify(() => - this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale) - ); + targetDocContextView.iconify(() => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed?*/ }, createViewFunc, docContext, finished)); } return ViewAdjustment.doNothing; }, @@ -281,56 +272,35 @@ export class DocumentManager { finished?.(); } else { // no timecode means we need to find the context view and focus on our target - const findView = (delay: number) => { - const retryDocView = this.getFirstDocumentView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it - if (retryDocView) { - // we found the target in the context. - Doc.linkFollowHighlight(retryDocView.rootDoc); - retryDocView.focus(targetDoc, { - willZoom, - afterFocus: (didFocus: boolean) => - new Promise(res => { - !noSelect && focusAndFinish(true); - res(ViewAdjustment.doNothing); - }), - }); // focus on the target in the context - } else if (delay > 1000) { - // we didn't find the target, so it must have moved out of the context. Go back to just creating it. - if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.rootDoc); - if (targetDoc.layout) { - // there will no layout for a TEXTANCHOR type document - createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target - } - } else { - setTimeout(() => findView(delay + 200), 200); - } - }; - setTimeout(() => findView(0), 0); + const retryDocView = this.getFirstDocumentView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it + if (retryDocView) { + // we found the target in the context. + Doc.linkFollowHighlight(retryDocView.rootDoc); + retryDocView.focus(targetDoc, { + ...options, + // originalTarget -- needed? + afterFocus: (didFocus: boolean) => + new Promise(res => { + !options.noSelect && focusAndFinish(true); + res(ViewAdjustment.doNothing); + }), + }); // focus on the target in the context + } else if (targetDoc.layout) { + // there will no layout for a TEXTANCHOR type document + createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target + } } } else { if (docContext.length && docContext[0]?.layoutKey === 'layout_icon') { const docContextView = this.getFirstDocumentView(docContext[0]); if (docContextView) { - return docContextView.iconify(() => - this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale) - ); + return docContextView.iconify(() => this.jumpToDocument(targetDoc, { ...options, originalTarget }, createViewFunc, docContext.slice(1, docContext.length), finished)); } } // there's no context view so we need to create one first and try again when that finishes createViewFunc( targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target - () => - this.jumpToDocument( - targetDoc, - willZoom, - (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), - docContext, - linkDoc, - true /* if target not found, get rid of context just created */, - originatingDoc, - finished, - originalTarget - ) + () => this.jumpToDocument(targetDoc, { ...options, originalTarget }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), docContext, finished) ); } } diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 37571ae01..916eee4b7 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -1,27 +1,27 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { BatchedArray } from "array-batcher"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { extname } from "path"; -import Measure, { ContentRect } from "react-measure"; -import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { List } from "../../../fields/List"; -import { listSpec } from "../../../fields/Schema"; -import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { BoolCast, Cast, NumCast } from "../../../fields/Types"; -import { AcceptableMedia, Upload } from "../../../server/SharedMediaTypes"; -import { Utils } from "../../../Utils"; -import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; -import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; -import { Networking } from "../../Network"; -import { FieldView, FieldViewProps } from "../../views/nodes/FieldView"; -import { DocumentManager } from "../DocumentManager"; -import "./DirectoryImportBox.scss"; -import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry"; -import React = require("react"); +import { BatchedArray } from 'array-batcher'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import { extname } from 'path'; +import Measure, { ContentRect } from 'react-measure'; +import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { listSpec } from '../../../fields/Schema'; +import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; +import { BoolCast, Cast, NumCast } from '../../../fields/Types'; +import { AcceptableMedia, Upload } from '../../../server/SharedMediaTypes'; +import { Utils } from '../../../Utils'; +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents'; +import { Networking } from '../../Network'; +import { FieldView, FieldViewProps } from '../../views/nodes/FieldView'; +import { DocumentManager } from '../DocumentManager'; +import './DirectoryImportBox.scss'; +import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from './ImportMetadataEntry'; +import React = require('react'); -const unsupported = ["text/html", "text/plain"]; +const unsupported = ['text/html', 'text/plain']; @observer export class DirectoryImportBox extends React.Component { @@ -29,7 +29,7 @@ export class DirectoryImportBox extends React.Component { @observable private top = 0; @observable private left = 0; private dimensions = 50; - @observable private phase = ""; + @observable private phase = ''; private disposer: Opt; @observable private entries: ImportMetadataEntry[] = []; @@ -40,7 +40,9 @@ export class DirectoryImportBox extends React.Component { @observable private uploading = false; @observable private removeHover = false; - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DirectoryImportBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(DirectoryImportBox, fieldKey); + } constructor(props: FieldViewProps) { super(props); @@ -71,7 +73,7 @@ export class DirectoryImportBox extends React.Component { handleSelection = async (e: React.ChangeEvent) => { runInAction(() => { this.uploading = true; - this.phase = "Initializing download..."; + this.phase = 'Initializing download...'; }); const docs: Doc[] = []; @@ -79,7 +81,7 @@ export class DirectoryImportBox extends React.Component { const files = e.target.files; if (!files || files.length === 0) return; - const directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0]; + const directory = (files.item(0) as any).webkitRelativePath.split('/', 1)[0]; const validated: File[] = []; for (let i = 0; i < files.length; i++) { @@ -100,7 +102,7 @@ export class DirectoryImportBox extends React.Component { const sizes: number[] = []; const modifiedDates: number[] = []; - runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`); + runInAction(() => (this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`)); const batched = BatchedArray.from(validated, { batchSize: 15 }); const uploads = await batched.batchedMapAsync>(async (batch, collector) => { @@ -109,23 +111,28 @@ export class DirectoryImportBox extends React.Component { modifiedDates.push(file.lastModified); }); collector.push(...(await Networking.UploadFilesToServer(batch))); - runInAction(() => this.completed += batch.length); + runInAction(() => (this.completed += batch.length)); }); - await Promise.all(uploads.map(async response => { - const { source: { type }, result } = response; - if (result instanceof Error) { - return; - } - const { accessPaths, exifData } = result; - const path = Utils.prepend(accessPaths.agnostic.client); - const document = type && await DocUtils.DocumentFromType(type, path, { _width: 300 }); - const { data, error } = exifData; - if (document) { - Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data }); - docs.push(document); - } - })); + await Promise.all( + uploads.map(async response => { + const { + source: { type }, + result, + } = response; + if (result instanceof Error) { + return; + } + const { accessPaths, exifData } = result; + const path = Utils.prepend(accessPaths.agnostic.client); + const document = type && (await DocUtils.DocumentFromType(type, path, { _width: 300 })); + const { data, error } = exifData; + if (document) { + Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data }); + docs.push(document); + } + }) + ); for (let i = 0; i < docs.length; i++) { const doc = docs[i]; @@ -146,7 +153,7 @@ export class DirectoryImportBox extends React.Component { _height: 500, _chromeHidden: true, x: NumCast(doc.x), - y: NumCast(doc.y) + offset + y: NumCast(doc.y) + offset, }; const parent = this.props.ContainingCollectionView; if (parent) { @@ -154,14 +161,14 @@ export class DirectoryImportBox extends React.Component { if (docs.length < 50) { importContainer = Docs.Create.MasonryDocument(docs, options); } else { - const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("size")]; + const headers = [new SchemaHeaderField('title'), new SchemaHeaderField('size')]; importContainer = Docs.Create.SchemaDocument(headers, docs, options); } - runInAction(() => this.phase = 'External: uploading files to Google Photos...'); + runInAction(() => (this.phase = 'External: uploading files to Google Photos...')); await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); - Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); + Doc.AddDocToList(Doc.GetProto(parent.props.Document), 'data', importContainer); !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); - DocumentManager.Instance.jumpToDocument(importContainer, true, undefined, []); + DocumentManager.Instance.jumpToDocument(importContainer, { willZoom: true }, undefined, []); } runInAction(() => { @@ -169,14 +176,14 @@ export class DirectoryImportBox extends React.Component { this.quota = 1; this.completed = 0; }); - } + }; componentDidMount() { - this.selector.current!.setAttribute("directory", ""); - this.selector.current!.setAttribute("webkitdirectory", ""); + this.selector.current!.setAttribute('directory', ''); + this.selector.current!.setAttribute('webkitdirectory', ''); this.disposer = reaction( () => this.completed, - completed => runInAction(() => this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`) + completed => runInAction(() => (this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`)) ); } @@ -193,7 +200,7 @@ export class DirectoryImportBox extends React.Component { const offset = this.dimensions / 2; this.left = bounds.width / 2 - offset; this.top = bounds.height / 2 - offset; - } + }; @action addMetadataEntry = async () => { @@ -201,8 +208,8 @@ export class DirectoryImportBox extends React.Component { entryDoc.checked = false; entryDoc.key = keyPlaceholder; entryDoc.value = valuePlaceholder; - Doc.AddDocToList(this.props.Document, "data", entryDoc); - } + Doc.AddDocToList(this.props.Document, 'data', entryDoc); + }; @action remove = async (entry: ImportMetadataEntry) => { @@ -217,7 +224,7 @@ export class DirectoryImportBox extends React.Component { } } } - } + }; render() { const dimensions = 50; @@ -228,193 +235,204 @@ export class DirectoryImportBox extends React.Component { const uploading = this.uploading; const showRemoveLabel = this.removeHover; const persistent = this.persistent; - let percent = `${completed / quota * 100}`; - percent = percent.split(".")[0]; - percent = percent.startsWith("100") ? "99" : percent; + let percent = `${(completed / quota) * 100}`; + percent = percent.split('.')[0]; + percent = percent.startsWith('100') ? '99' : percent; const marginOffset = (percent.length === 1 ? 5 : 0) - 1.6; - const message = {this.phase}; - const centerPiece = this.phase.includes("Google Photos") ? - - :
{this.phase}; + const centerPiece = this.phase.includes('Google Photos') ? ( + + ) : ( +
{percent}%
; + color: 'white', + marginLeft: this.left + marginOffset, + }}> + {percent}% +
+ ); return ( - {({ measureRef }) => -
+ {({ measureRef }) => ( +
{message} + position: 'absolute', + display: 'none', + }} + />