diff options
author | Sam Wilkins <samwilkins333@gmail.com> | 2019-07-31 03:39:55 -0400 |
---|---|---|
committer | Sam Wilkins <samwilkins333@gmail.com> | 2019-07-31 03:39:55 -0400 |
commit | 7e8778b06dacab6e9e6dedc562c10898f7075a3b (patch) | |
tree | 744d33c145c7325b3785e865517f5add7fb7a857 /src | |
parent | 8a87f7110b56ca96b3960f6fb3917c7ed8c7a814 (diff) | |
parent | b6fa309cea934d250fe992e70e1e268f344659b5 (diff) |
merged with master
Diffstat (limited to 'src')
68 files changed, 2898 insertions, 750 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index cb460799f..258acd9cd 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,5 +1,5 @@ import * as OpenSocket from 'socket.io-client'; -import { MessageStore, Diff } from "./../server/Message"; +import { MessageStore, Diff, YoutubeQueryTypes } from "./../server/Message"; import { Opt } from '../new_fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; @@ -156,6 +156,20 @@ export namespace DocServer { return _GetRefField(id); } + export async function getYoutubeChannels() { + let apiKey = await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels }); + return apiKey; + } + + export function getYoutubeVideos(videoTitle: string, callBack: (videos: any[]) => void) { + Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.SearchVideo, userInput: videoTitle }, callBack); + } + + export function getYoutubeVideoDetails(videoIds: string, callBack: (videoDetails: any[]) => void) { + Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.VideoDetails, videoIds: videoIds }, callBack); + } + + /** * Given a list of Doc GUIDs, this utility function will asynchronously attempt to each id's associated * field, first looking in the RefField cache and then communicating with diff --git a/src/client/apis/youtube/YoutubeBox.scss b/src/client/apis/youtube/YoutubeBox.scss new file mode 100644 index 000000000..eabdbb1ac --- /dev/null +++ b/src/client/apis/youtube/YoutubeBox.scss @@ -0,0 +1,126 @@ +.youtubeBox-cont { + ul { + list-style-type: none; + padding-inline-start: 10px; + } + + + li { + margin: 4px; + display: inline-flex; + } + + li:hover { + cursor: pointer; + opacity: 0.8; + } + + .search_wrapper { + width: 100%; + display: inline-flex; + height: 175px; + + .video_duration { + // margin: 0; + // padding: 0; + border: 0; + background: transparent; + display: inline-block; + position: relative; + bottom: 25px; + left: 85%; + margin: 4px; + color: #FFFFFF; + background-color: rgba(0, 0, 0, 0.80); + padding: 2px 4px; + border-radius: 2px; + letter-spacing: .5px; + font-size: 1.2rem; + font-weight: 500; + line-height: 1.2rem; + + } + + .textual_info { + font-family: Arial, Helvetica, sans-serif; + + .videoTitle { + margin-left: 4px; + // display: inline-block; + color: #0D0D0D; + -webkit-line-clamp: 2; + display: block; + max-height: 4.8rem; + overflow: hidden; + font-size: 1.8rem; + font-weight: 400; + line-height: 2.4rem; + -webkit-box-orient: vertical; + text-overflow: ellipsis; + white-space: normal; + display: -webkit-box; + } + + .channelName { + color: #606060; + margin-left: 4px; + font-size: 1.3rem; + font-weight: 400; + line-height: 1.8rem; + text-transform: none; + margin-top: 0px; + display: inline-block; + } + + .video_description { + margin-left: 4px; + // font-size: 12px; + color: #606060; + padding-top: 8px; + margin-bottom: 8px; + display: block; + line-height: 1.8rem; + max-height: 4.2rem; + overflow: hidden; + font-size: 1.3rem; + font-weight: 400; + text-transform: none; + } + + .publish_time { + //display: inline-block; + margin-left: 8px; + padding: 0; + border: 0; + background: transparent; + color: #606060; + max-width: 100%; + line-height: 1.8rem; + max-height: 3.6rem; + overflow: hidden; + font-size: 1.3rem; + font-weight: 400; + text-transform: none; + } + + .viewCount { + + margin-left: 8px; + padding: 0; + border: 0; + background: transparent; + color: #606060; + max-width: 100%; + line-height: 1.8rem; + max-height: 3.6rem; + overflow: hidden; + font-size: 1.3rem; + font-weight: 400; + text-transform: none; + } + + + + } + } +}
\ No newline at end of file diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx new file mode 100644 index 000000000..dc142802c --- /dev/null +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -0,0 +1,362 @@ +import { action, observable, runInAction } from 'mobx'; +import { observer } from "mobx-react"; +import { Doc, DocListCastAsync } from "../../../new_fields/Doc"; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { Utils } from "../../../Utils"; +import { DocServer } from "../../DocServer"; +import { Docs } from "../../documents/Documents"; +import { DocumentDecorations } from "../../views/DocumentDecorations"; +import { InkingControl } from "../../views/InkingControl"; +import { FieldView, FieldViewProps } from "../../views/nodes/FieldView"; +import "../../views/nodes/WebBox.scss"; +import "./YoutubeBox.scss"; +import React = require("react"); + +interface VideoTemplate { + thumbnailUrl: string; + videoTitle: string; + videoId: string; + duration: string; + channelTitle: string; + viewCount: string; + publishDate: string; + videoDescription: string; +} + +/** + * This class models the youtube search document that can be dropped on to canvas. + */ +@observer +export class YoutubeBox extends React.Component<FieldViewProps> { + + @observable YoutubeSearchElement: HTMLInputElement | undefined; + @observable searchResultsFound: boolean = false; + @observable searchResults: any[] = []; + @observable videoClicked: boolean = false; + @observable selectedVideoUrl: string = ""; + @observable lisOfBackUp: JSX.Element[] = []; + @observable videoIds: string | undefined; + @observable videoDetails: any[] = []; + @observable curVideoTemplates: VideoTemplate[] = []; + + + public static LayoutString() { return FieldView.LayoutString(YoutubeBox); } + + /** + * When component mounts, last search's results are laoded in based on the back up stored + * in the document of the props. + */ + async componentWillMount() { + //DocServer.getYoutubeChannels(); + let castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc); + let awaitedBackUp = await castedSearchBackUp; + let castedDetailBackUp = Cast(this.props.Document.cachedDetails, Doc); + let awaitedDetails = await castedDetailBackUp; + + + if (awaitedBackUp) { + + + let jsonList = await DocListCastAsync(awaitedBackUp!.json); + let jsonDetailList = await DocListCastAsync(awaitedDetails!.json); + + if (jsonList!.length !== 0) { + runInAction(() => this.searchResultsFound = true); + let index = 0; + //getting the necessary information from backUps and building templates that will be used to map in render + for (let video of jsonList!) { + + let videoId = await Cast(video.id, Doc); + let id = StrCast(videoId!.videoId); + let snippet = await Cast(video.snippet, Doc); + let videoTitle = this.filterYoutubeTitleResult(StrCast(snippet!.title)); + let thumbnail = await Cast(snippet!.thumbnails, Doc); + let thumbnailMedium = await Cast(thumbnail!.medium, Doc); + let thumbnailUrl = StrCast(thumbnailMedium!.url); + let videoDescription = StrCast(snippet!.description); + let pusblishDate = (this.roundPublishTime(StrCast(snippet!.publishedAt)))!; + let channelTitle = StrCast(snippet!.channelTitle); + let duration: string; + let viewCount: string; + if (jsonDetailList!.length !== 0) { + let contentDetails = await Cast(jsonDetailList![index].contentDetails, Doc); + let statistics = await Cast(jsonDetailList![index].statistics, Doc); + duration = this.convertIsoTimeToDuration(StrCast(contentDetails!.duration)); + viewCount = this.abbreviateViewCount(parseInt(StrCast(statistics!.viewCount)))!; + } + index = index + 1; + let newTemplate: VideoTemplate = { videoId: id, videoTitle: videoTitle, thumbnailUrl: thumbnailUrl, publishDate: pusblishDate, channelTitle: channelTitle, videoDescription: videoDescription, duration: duration!, viewCount: viewCount! }; + runInAction(() => this.curVideoTemplates.push(newTemplate)); + } + } + } + } + + _ignore = 0; + onPreWheel = (e: React.WheelEvent) => { + this._ignore = e.timeStamp; + } + onPrePointer = (e: React.PointerEvent) => { + this._ignore = e.timeStamp; + } + onPostPointer = (e: React.PointerEvent) => { + if (this._ignore !== e.timeStamp) { + e.stopPropagation(); + } + } + onPostWheel = (e: React.WheelEvent) => { + if (this._ignore !== e.timeStamp) { + e.stopPropagation(); + } + } + + /** + * Function that submits the title entered by user on enter press. + */ + onEnterKeyDown = (e: React.KeyboardEvent) => { + if (e.keyCode === 13) { + let submittedTitle = this.YoutubeSearchElement!.value; + this.YoutubeSearchElement!.value = ""; + this.YoutubeSearchElement!.blur(); + DocServer.getYoutubeVideos(submittedTitle, this.processesVideoResults); + + } + } + + /** + * The callback that is passed in to server, which functions as a way to + * get videos that is returned by search. It also makes a call to server + * to get details for the videos found. + */ + @action + processesVideoResults = (videos: any[]) => { + this.searchResults = videos; + if (this.searchResults.length > 0) { + this.searchResultsFound = true; + this.videoIds = ""; + videos.forEach((video) => { + if (this.videoIds === "") { + this.videoIds = video.id.videoId; + } else { + this.videoIds = this.videoIds! + ", " + video.id.videoId; + } + }); + //Asking for details that include duration and viewCount from server for videoIds + DocServer.getYoutubeVideoDetails(this.videoIds, this.processVideoDetails); + this.backUpSearchResults(videos); + if (this.videoClicked) { + this.videoClicked = false; + } + } + } + + /** + * The callback that is given to server to process and receive returned details about the videos. + */ + @action + processVideoDetails = (videoDetails: any[]) => { + this.videoDetails = videoDetails; + this.props.Document.cachedDetails = Docs.Get.DocumentHierarchyFromJson(videoDetails, "detailBackUp"); + } + + /** + * The function that stores the search results in the props document. + */ + backUpSearchResults = (videos: any[]) => { + this.props.Document.cachedSearchResults = Docs.Get.DocumentHierarchyFromJson(videos, "videosBackUp"); + } + + /** + * The function that filters out escaped characters returned by the api + * in the title of the videos. + */ + filterYoutubeTitleResult = (resultTitle: string) => { + let processedTitle: string = resultTitle.ReplaceAll("&", "&"); + processedTitle = processedTitle.ReplaceAll("'", "'"); + processedTitle = processedTitle.ReplaceAll(""", "\""); + return processedTitle; + } + + + + /** + * The function that converts ISO date, which is passed in, to normal date and finds the + * difference between today's date and that date, in terms of "ago" to imitate youtube. + */ + roundPublishTime = (publishTime: string) => { + let date = new Date(publishTime).getTime(); + let curDate = new Date().getTime(); + let timeDif = curDate - date; + let totalSeconds = timeDif / 1000; + let totalMin = totalSeconds / 60; + let totalHours = totalMin / 60; + let totalDays = totalHours / 24; + let totalMonths = totalDays / 30.417; + let totalYears = totalMonths / 12; + + + let truncYears = Math.trunc(totalYears); + let truncMonths = Math.trunc(totalMonths); + let truncDays = Math.trunc(totalDays); + let truncHours = Math.trunc(totalHours); + let truncMin = Math.trunc(totalMin); + let truncSec = Math.trunc(totalSeconds); + + let pluralCase = ""; + + if (truncYears !== 0) { + truncYears > 1 ? pluralCase = "s" : pluralCase = ""; + return truncYears + " year" + pluralCase + " ago"; + } else if (truncMonths !== 0) { + truncMonths > 1 ? pluralCase = "s" : pluralCase = ""; + return truncMonths + " month" + pluralCase + " ago"; + } else if (truncDays !== 0) { + truncDays > 1 ? pluralCase = "s" : pluralCase = ""; + return truncDays + " day" + pluralCase + " ago"; + } else if (truncHours !== 0) { + truncHours > 1 ? pluralCase = "s" : pluralCase = ""; + return truncHours + " hour" + pluralCase + " ago"; + } else if (truncMin !== 0) { + truncMin > 1 ? pluralCase = "s" : pluralCase = ""; + return truncMin + " minute" + pluralCase + " ago"; + } else if (truncSec !== 0) { + truncSec > 1 ? pluralCase = "s" : pluralCase = ""; + return truncSec + " second" + pluralCase + " ago"; + } + } + + /** + * The function that converts the passed in ISO time to normal duration time. + */ + convertIsoTimeToDuration = (isoDur: string) => { + + let convertedTime = isoDur.replace(/D|H|M/g, ":").replace(/P|T|S/g, "").split(":"); + + if (1 === convertedTime.length) { + 2 !== convertedTime[0].length && (convertedTime[0] = "0" + convertedTime[0]), convertedTime[0] = "0:" + convertedTime[0]; + } else { + for (var r = 1, l = convertedTime.length - 1; l >= r; r++) { + 2 !== convertedTime[r].length && (convertedTime[r] = "0" + convertedTime[r]); + } + } + + return convertedTime.join(":"); + } + + /** + * The function that rounds the viewCount to the nearest + * thousand, million or billion, given a viewCount number. + */ + abbreviateViewCount = (viewCount: number) => { + if (viewCount < 1000) { + return viewCount.toString(); + } else if (viewCount >= 1000 && viewCount < 1000000) { + return (Math.trunc(viewCount / 1000)) + "K"; + } else if (viewCount >= 1000000 && viewCount < 1000000000) { + return (Math.trunc(viewCount / 1000000)) + "M"; + } else if (viewCount >= 1000000000) { + return (Math.trunc(viewCount / 1000000000)) + "B"; + } + } + + /** + * The function that is called to decide on what'll be rendered by the component. + * It renders search Results if found. If user didn't do a new search, it renders from the videoTemplates + * generated by the backUps. If none present, renders nothing. + */ + renderSearchResultsOrVideo = () => { + if (this.searchResultsFound) { + if (this.searchResults.length !== 0) { + return <ul> + {this.searchResults.map((video, index) => { + let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title); + let channelTitle = video.snippet.channelTitle; + let videoDescription = video.snippet.description; + let pusblishDate = this.roundPublishTime(video.snippet.publishedAt); + let duration; + let viewCount; + if (this.videoDetails.length !== 0) { + duration = this.convertIsoTimeToDuration(this.videoDetails[index].contentDetails.duration); + viewCount = this.abbreviateViewCount(this.videoDetails[index].statistics.viewCount); + } + + + return <li onClick={() => this.embedVideoOnClick(video.id.videoId, filteredTitle)} key={Utils.GenerateGuid()}> + <div className="search_wrapper"> + <div style={{ backgroundColor: "yellow" }}> + <img src={video.snippet.thumbnails.medium.url} /> + <span className="video_duration">{duration}</span> + </div> + <div className="textual_info"> + <span className="videoTitle">{filteredTitle}</span> + <span className="channelName">{channelTitle}</span> + <span className="viewCount">{viewCount}</span> + <span className="publish_time">{pusblishDate}</span> + <p className="video_description">{videoDescription}</p> + + </div> + </div> + </li>; + })} + </ul>; + } else if (this.curVideoTemplates.length !== 0) { + return <ul> + {this.curVideoTemplates.map((video: VideoTemplate) => { + return <li onClick={() => this.embedVideoOnClick(video.videoId, video.videoTitle)} key={Utils.GenerateGuid()}> + <div className="search_wrapper"> + <div style={{ backgroundColor: "yellow" }}> + <img src={video.thumbnailUrl} /> + <span className="video_duration">{video.duration}</span> + </div> + <div className="textual_info"> + <span className="videoTitle">{video.videoTitle}</span> + <span className="channelName">{video.channelTitle}</span> + <span className="viewCount">{video.viewCount}</span> + <span className="publish_time">{video.publishDate}</span> + <p className="video_description">{video.videoDescription}</p> + </div> + </div> + </li>; + })} + </ul>; + } + } else { + return (null); + } + } + + /** + * Given a videoId and title, creates a new youtube embedded url, and uses that + * to create a new video document. + */ + @action + embedVideoOnClick = (videoId: string, filteredTitle: string) => { + let embeddedUrl = "https://www.youtube.com/embed/" + videoId; + this.selectedVideoUrl = embeddedUrl; + let addFunction = this.props.addDocument!; + let newVideoX = NumCast(this.props.Document.x); + let newVideoY = NumCast(this.props.Document.y) + NumCast(this.props.Document.height); + + addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, width: 400, height: 315, x: newVideoX, y: newVideoY })); + this.videoClicked = true; + } + + render() { + let content = + <div className="youtubeBox-cont" style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> + <input type="text" placeholder="Search for a video" onKeyDown={this.onEnterKeyDown} style={{ height: 40, width: "100%", border: "1px solid black", padding: 5, textAlign: "center" }} ref={(e) => this.YoutubeSearchElement = e!} /> + {this.renderSearchResultsOrVideo()} + </div>; + + let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; + + let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); + return ( + <> + <div className={classname} > + {content} + </div> + {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />} + </>); + } +}
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index d1b3071ed..2a1f63d59 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -22,7 +22,7 @@ import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { IconBox } from "../views/nodes/IconBox"; import { Field, Doc, Opt } from "../../new_fields/Doc"; import { OmitKeys, JSONUtils } from "../../Utils"; -import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField"; +import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField"; import { HtmlField } from "../../new_fields/HtmlField"; import { List } from "../../new_fields/List"; import { Cast, NumCast, StrCast, ToConstructor, InterfaceValue, FieldValue } from "../../new_fields/Types"; @@ -33,6 +33,7 @@ import { dropActionType } from "../util/DragManager"; import { DateField } from "../../new_fields/DateField"; import { UndoManager } from "../util/UndoManager"; import { RouteStore } from "../../server/RouteStore"; +import { YoutubeBox } from "../apis/youtube/YoutubeBox"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { LinkManager } from "../util/LinkManager"; import { DocumentManager } from "../util/DocumentManager"; @@ -60,7 +61,8 @@ export enum DocumentType { LINKDOC = "linkdoc", BUTTON = "button", TEMPLATE = "template", - EXTENSION = "extension" + EXTENSION = "extension", + YOUTUBE = "youtube", } export interface DocumentOptions { @@ -165,6 +167,10 @@ export namespace Docs { [DocumentType.LINKDOC, { data: new List<Doc>(), layout: { view: EmptyBox }, + options: {} + }], + [DocumentType.YOUTUBE, { + layout: { view: YoutubeBox } }], [DocumentType.BUTTON, { layout: { view: ButtonBox }, @@ -340,6 +346,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(new URL(url)), options); } + export function YoutubeDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(new URL(url)), options); + } + export function AudioDocument(url: string, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options); } @@ -404,7 +414,7 @@ export namespace Docs { } export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Freeform }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }); } export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) { @@ -412,15 +422,15 @@ export namespace Docs { } export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Tree }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Tree }); } export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Stacking }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Stacking }); } export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Masonry }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Masonry }); } export function ButtonDocument(options?: DocumentOptions) { @@ -505,7 +515,7 @@ export namespace Docs { const convertObject = (object: any, title?: string): Doc => { let target = new Doc(), result: Opt<Field>; Object.keys(object).map(key => (result = toField(object[key], key)) && (target[key] = result)); - title && (target.title = title); + title && !target.title && (target.title = title); return target; }; diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts index fa9e2e5af..c38f84551 100644 --- a/src/client/util/ProsemirrorExampleTransfer.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -47,6 +47,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: bind("Mod-i", toggleMark(type)); bind("Mod-I", toggleMark(type)); } + if (type = schema.marks.underline) { + bind("Mod-u", toggleMark(type)); + bind("Mod-U", toggleMark(type)); + } if (type = schema.marks.code) { bind("Mod-`", toggleMark(type)); } diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 269de0f42..ce9e29b26 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -283,7 +283,7 @@ export const marks: { [index: string]: MarkSpec } = { }, highlight: { - parseDOM: [{ style: 'background: #d9dbdd' }], + parseDOM: [{ style: 'color: blue' }], toDOM() { return ['span', { style: 'color: blue' @@ -291,6 +291,15 @@ export const marks: { [index: string]: MarkSpec } = { } }, + search_highlight: { + parseDOM: [{ style: 'background: yellow' }], + toDOM() { + return ['span', { + style: 'background: yellow' + }]; + } + }, + // :: MarkSpec Code font mark. Represented as a `<code>` element. code: { diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index 40ac3abb9..ebf833dbe 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -18,7 +18,8 @@ .ProseMirror-menuitem { margin-right: 3px; display: inline-block; - z-index: 100000; + z-index: 50000; + position: relative; } .ProseMirror-menuseparator { @@ -67,7 +68,7 @@ } .ProseMirror-menu-dropdown-menu { - z-index: 100000; + z-index: 50000; min-width: 6em; background: white; position: absolute; @@ -235,8 +236,8 @@ } .tooltipMenu { - position: relative; - z-index: 2000; + position: absolute; + z-index: 20000; background: #121721; border: 1px solid silver; border-radius: 15px; @@ -247,7 +248,7 @@ //transform: translateX(-50%); transform: translateY(-85px); pointer-events: all; - height: 30px; + height: fit-content; width:550px; .ProseMirror-example-setup-style hr { padding: 2px 10px; @@ -264,28 +265,40 @@ } } -// .tooltipMenu:before { -// content: ""; -// height: 0; width: 0; -// position: absolute; -// left: 50%; -// margin-left: -5px; -// bottom: -6px; -// border: 5px solid transparent; -// border-bottom-width: 0; -// border-top-color: silver; -// } -// .tooltipMenu:after { -// content: ""; -// height: 0; width: 0; -// position: absolute; -// left: 50%; -// margin-left: -5px; -// bottom: -4.5px; -// border: 5px solid transparent; -// border-bottom-width: 0; -// border-top-color: $dark-color; -// } +.tooltipExtras { + position: absolute; + z-index: 20000; + background: #121721; + border: 1px solid silver; + border-radius: 15px; + //height: 60px; + //padding: 2px 10px; + //margin-top: 100px; + //-webkit-transform: translateX(-50%); + //transform: translateX(-50%); + transform: translateY(-115px); + pointer-events: all; + height: 25px; + width:fit-content; + .ProseMirror-example-setup-style hr { + padding: 2px 10px; + border: none; + margin: 1em 0; + } + + .ProseMirror-example-setup-style hr:after { + content: ""; + display: block; + height: 1px; + background-color: silver; + line-height: 2px; + } +} + +.wrapper { + position: absolute; + pointer-events: all; +} .menuicon { display: inline-block; @@ -298,6 +311,7 @@ cursor: pointer; text-align: center; min-width: 10px; + } .strong, .heading { font-weight: bold; } .em { font-style: italic; } @@ -310,9 +324,32 @@ padding-right: 0px; } .summarize{ - //margin-left: 15px; color: white; height: 20px; - // background-color: white; text-align: center; + } + + .brush{ + display: inline-block; + width: 1em; + height: 1em; + stroke-width: 0; + stroke: currentColor; + fill: currentColor; + margin-right: 15px; + } + + .brush-active{ + display: inline-block; + width: 1em; + height: 1em; + stroke-width: 3; + stroke: greenyellow; + fill: greenyellow; + margin-right: 15px; + } + + .dragger{ + color: #eee; + margin-left: 5px; }
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 6214b568c..d33a52d7f 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -1,4 +1,6 @@ -import { action } from "mobx"; +import { action, observable, observe } from "mobx"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTag, faPlus, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; import { Dropdown, MenuItem, icons, } from "prosemirror-menu"; //no import css import { EditorState, NodeSelection, TextSelection, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; @@ -18,8 +20,13 @@ import { DocServer } from "../DocServer"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { DocumentManager } from "./DocumentManager"; import { Id } from "../../new_fields/FieldSymbols"; -import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox"; +import { FormattedTextBoxProps, FormattedTextBox } from "../views/nodes/FormattedTextBox"; +import { typeAlias } from "babel-types"; +import React, { Children } from "react"; +import ReactDOM from "react-dom"; import { Utils } from "../../Utils"; +import { LinkManager } from "./LinkManager"; +import { bool } from "prop-types"; //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. export class TooltipTextMenu { @@ -33,6 +40,9 @@ export class TooltipTextMenu { private fontSizeToNum: Map<MarkType, number>; private fontStylesToName: Map<MarkType, string>; private listTypeToIcon: Map<NodeType, string>; + //private link: HTMLAnchorElement; + private wrapper: HTMLDivElement; + private extras: HTMLDivElement; private linkEditor?: HTMLDivElement; private linkText?: HTMLDivElement; @@ -46,13 +56,42 @@ export class TooltipTextMenu { private _collapseBtn?: MenuItem; + private _brushMarks?: Set<Mark>; + private _brushIsEmpty: boolean = true; + private _brushdom?: Node; + + private _marksToDoms: Map<Mark, HTMLSpanElement> = new Map(); + + private _collapsed: boolean = false; + + @observable + private _storedMarks: Mark<any>[] | null | undefined; + + constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) { this.view = view; this.editorProps = editorProps; + + this.wrapper = document.createElement("div"); this.tooltip = document.createElement("div"); + this.extras = document.createElement("div"); + + this.wrapper.appendChild(this.extras); + this.wrapper.appendChild(this.tooltip); + this.tooltip.className = "tooltipMenu"; + this.extras.className = "tooltipExtras"; + this.wrapper.className = "wrapper"; + + const dragger = document.createElement("span"); + dragger.className = "dragger"; + dragger.textContent = ">>>"; + this.extras.appendChild(dragger); + + this.dragElement(dragger); + + this._storedMarks = this.view.state.storedMarks; - this.dragElement(this.tooltip); // this.createCollapse(); // if (this._collapseBtn) { // this.tooltip.appendChild(this._collapseBtn.render(this.view).dom); @@ -71,13 +110,23 @@ export class TooltipTextMenu { { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript", "Superscript") }, { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript", "Subscript") }, { command: toggleMark(schema.marks.highlight), dom: this.icon("H", 'blue', 'Blue') } - // { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, - // { command: wrapInList(schema.nodes.ordered_list), dom: this.icon("1)", "bullets") }, - // { command: lift, dom: this.icon("<", "lift") }, ]; + + this._marksToDoms = new Map(); //add menu items items.forEach(({ dom, command }) => { this.tooltip.appendChild(dom); + switch (dom.title) { + case "Bold": + this._marksToDoms.set(schema.mark(schema.marks.strong), dom); + break; + case "Italic": + this._marksToDoms.set(schema.mark(schema.marks.em), dom); + break; + case "Underline": + this._marksToDoms.set(schema.mark(schema.marks.underline), dom); + break; + } //pointer down handler to activate button effects dom.addEventListener("pointerdown", e => { @@ -86,12 +135,17 @@ export class TooltipTextMenu { if (dom.contains(e.target as Node)) { e.stopPropagation(); command(view.state, view.dispatch, view); + // if (this.view.state.selection.empty) { + // if (dom.style.color === "white") { dom.style.color = "greenyellow"; } + // else { dom.style.color = "white"; } + // } } }); }); this.updateLinkMenu(); + //list of font styles this.fontStylesToName = new Map(); this.fontStylesToName.set(schema.marks.timesNewRoman, "Times New Roman"); @@ -125,21 +179,23 @@ export class TooltipTextMenu { this.listTypeToIcon.set(schema.nodes.ordered_list, "1)"); this.listTypes = Array.from(this.listTypeToIcon.keys()); + //custom tools // this.tooltip.appendChild(this.createLink().render(this.view).dom); + this._brushdom = this.createBrush().render(this.view).dom; + this.tooltip.appendChild(this._brushdom); + this.tooltip.appendChild(this.createLink().render(this.view).dom); this.tooltip.appendChild(this.createStar().render(this.view).dom); - - this.updateListItemDropdown(":", this.listTypeBtnDom); this.update(view, undefined); //view.dom.parentNode!.parentNode!.insertBefore(this.tooltip, view.dom.parentNode); - // quick and dirty null check + // add tooltip to outerdiv to circumvent scaling problem const outer_div = this.editorProps.outer_div; - outer_div && outer_div(this.tooltip); + outer_div && outer_div(this.wrapper); } //label of dropdown will change to given label @@ -164,6 +220,8 @@ export class TooltipTextMenu { this.fontSizeDom = newfontSizeDom; } + // Make the DIV element draggable + //label of dropdown will change to given label updateFontStyleDropdown(label: string) { //filtering function - might be unecessary @@ -259,6 +317,8 @@ export class TooltipTextMenu { }, hideSource: false }); + e.stopPropagation(); + e.preventDefault(); }; this.linkEditor.appendChild(this.linkDrag); // this.linkEditor.appendChild(this.linkText); @@ -285,6 +345,7 @@ export class TooltipTextMenu { if (elmnt) { // if present, the header is where you move the DIV from: elmnt.onpointerdown = dragMouseDown; + elmnt.ondblclick = onClick; } const self = this; @@ -299,6 +360,17 @@ export class TooltipTextMenu { document.onpointermove = elementDrag; } + function onClick(e: MouseEvent) { + self._collapsed = !self._collapsed; + const children = self.wrapper.childNodes; + if (self._collapsed && children.length > 1) { + self.wrapper.removeChild(self.tooltip); + } + else { + self.wrapper.appendChild(self.tooltip); + } + } + function elementDrag(e: PointerEvent) { e = e || window.event; //e.preventDefault(); @@ -308,8 +380,11 @@ export class TooltipTextMenu { pos3 = e.clientX; pos4 = e.clientY; // set the element's new position: - elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; - elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; + // elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; + // elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; + + self.wrapper.style.top = (self.wrapper.offsetTop - pos2) + "px"; + self.wrapper.style.left = (self.wrapper.offsetLeft - pos1) + "px"; } function closeDragElement() { @@ -334,6 +409,27 @@ export class TooltipTextMenu { link = node && node.marks.find(m => m.type.name === "link"); } + deleteLink = () => { + let node = this.view.state.selection.$from.nodeAfter; + let link = node && node.marks.find(m => m.type.name === "link"); + let href = link!.attrs.href; + if (href) { + if (href.indexOf(Utils.prepend("/doc/")) === 0) { + const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + if (linkclicked) { + DocServer.GetRefField(linkclicked).then(async linkDoc => { + if (linkDoc instanceof Doc) { + LinkManager.Instance.deleteLink(linkDoc); + this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link)); + } + }); + } + } + } + + + } + public static insertStar(state: EditorState<any>, dispatch: any) { let newNode = schema.nodes.star.create({ visibility: false, text: state.selection.content(), textslice: state.selection.content().toJSON(), textlen: state.selection.to - state.selection.from }); if (dispatch) { @@ -367,7 +463,7 @@ export class TooltipTextMenu { } //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text - changeToMarkInGroup = (markType: MarkType, view: EditorView, fontMarks: MarkType[]) => { + changeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => { let { $cursor, ranges } = view.state.selection as TextSelection; let state = view.state; let dispatch = view.dispatch; @@ -393,17 +489,23 @@ export class TooltipTextMenu { } } }); - // fontsize - if (markType.name[0] === 'p') { - let size = this.fontSizeToNum.get(markType); - if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } + + if (markType) { + // fontsize + if (markType.name[0] === 'p') { + let size = this.fontSizeToNum.get(markType); + if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } + } + else { + let fontName = this.fontStylesToName.get(markType); + if (fontName) { this.updateFontStyleDropdown(fontName); } + } + //actually apply font + return toggleMark(markType)(view.state, view.dispatch, view); } else { - let fontName = this.fontStylesToName.get(markType); - if (fontName) { this.updateFontStyleDropdown(fontName); } + return; } - //actually apply font - return toggleMark(markType)(view.state, view.dispatch, view); } //remove all node typeand apply the passed-in one to the selected text @@ -446,6 +548,85 @@ export class TooltipTextMenu { }); } + deleteLinkItem() { + const icon = { + height: 16, width: 16, + path: "M15.898,4.045c-0.271-0.272-0.713-0.272-0.986,0l-4.71,4.711L5.493,4.045c-0.272-0.272-0.714-0.272-0.986,0s-0.272,0.714,0,0.986l4.709,4.711l-4.71,4.711c-0.272,0.271-0.272,0.713,0,0.986c0.136,0.136,0.314,0.203,0.492,0.203c0.179,0,0.357-0.067,0.493-0.203l4.711-4.711l4.71,4.711c0.137,0.136,0.314,0.203,0.494,0.203c0.178,0,0.355-0.067,0.492-0.203c0.273-0.273,0.273-0.715,0-0.986l-4.711-4.711l4.711-4.711C16.172,4.759,16.172,4.317,15.898,4.045z" + }; + return new MenuItem({ + title: "Delete Link", + label: "X", + icon: icon, + css: "color: red", + class: "summarize", + execEvent: "", + run: (state, dispatch) => { + this.deleteLink(); + } + }); + } + + createBrush(active: boolean = false) { + const icon = { + height: 32, width: 32, + path: "M30.828 1.172c-1.562-1.562-4.095-1.562-5.657 0l-5.379 5.379-3.793-3.793-4.243 4.243 3.326 3.326-14.754 14.754c-0.252 0.252-0.358 0.592-0.322 0.921h-0.008v5c0 0.552 0.448 1 1 1h5c0 0 0.083 0 0.125 0 0.288 0 0.576-0.11 0.795-0.329l14.754-14.754 3.326 3.326 4.243-4.243-3.793-3.793 5.379-5.379c1.562-1.562 1.562-4.095 0-5.657zM5.409 30h-3.409v-3.409l14.674-14.674 3.409 3.409-14.674 14.674z" + }; + return new MenuItem({ + title: "Brush tool", + label: "Brush tool", + icon: icon, + css: "color:white;", + class: active ? "brush-active" : "brush", + execEvent: "", + run: (state, dispatch) => { + this.brush_function(state, dispatch); + }, + active: (state) => { + return true; + } + }); + } + + // selectionchanged event handler + + brush_function(state: EditorState<any>, dispatch: any) { + if (this._brushIsEmpty) { + const selected_marks = this.getMarksInSelection(this.view.state); + if (this._brushdom) { + if (selected_marks.size >= 0) { + this._brushMarks = selected_marks; + const newbrush = this.createBrush(true).render(this.view).dom; + this.tooltip.replaceChild(newbrush, this._brushdom); + this._brushdom = newbrush; + this._brushIsEmpty = !this._brushIsEmpty; + } + } + } + else { + let { from, to, $from } = this.view.state.selection; + if (this._brushdom) { + if (!this.view.state.selection.empty && $from && $from.nodeAfter) { + if (this._brushMarks && to - from > 0) { + this.view.dispatch(this.view.state.tr.removeMark(from, to)); + this._brushMarks.forEach((mark: Mark) => { + const markType = mark.type; + this.changeToMarkInGroup(markType, this.view, []); + + }); + } + } + else { + const newbrush = this.createBrush(false).render(this.view).dom; + this.tooltip.replaceChild(newbrush, this._brushdom); + this._brushdom = newbrush; + this._brushIsEmpty = !this._brushIsEmpty; + } + } + } + + + } + createCollapse() { this._collapseBtn = new MenuItem({ title: "Collapse", @@ -601,20 +782,29 @@ export class TooltipTextMenu { }; } - getMarksInSelection(state: EditorState<any>, targets: MarkType<any>[]) { - let found: Mark<any>[] = []; + getMarksInSelection(state: EditorState<any>) { + let found = new Set<Mark>(); let { from, to } = state.selection as TextSelection; state.doc.nodesBetween(from, to, (node) => { let marks = node.marks; if (marks) { marks.forEach(m => { - if (targets.includes(m.type)) found.push(m); + found.add(m); }); } }); return found; } + reset_mark_doms() { + let iterator = this._marksToDoms.values(); + let next = iterator.next(); + while (!next.done) { + next.value.style.color = "white"; + next = iterator.next(); + } + } + //updates the tooltip menu when the selection changes update(view: EditorView, lastState: EditorState | undefined) { let state = view.state; @@ -622,13 +812,13 @@ export class TooltipTextMenu { if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return; + this.reset_mark_doms(); + // Hide the tooltip if the selection is empty if (state.selection.empty) { //this.tooltip.style.display = "none"; //return; } - - //UPDATE LIST ITEM DROPDOWN //UPDATE FONT STYLE DROPDOWN @@ -666,12 +856,55 @@ export class TooltipTextMenu { } } this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks)); + + this.update_mark_doms(); + } + + public mark_key_pressed(marks: Mark<any>[]) { + if (this.view.state.selection.empty) { + if (marks) this._activeMarks = marks; + this.update_mark_doms(); + } + } + + update_mark_doms() { + this.reset_mark_doms(); + let foundlink = false; + let children = this.extras.childNodes; + this._activeMarks.forEach((mark) => { + if (this._marksToDoms.has(mark)) { + let dom = this._marksToDoms.get(mark); + if (dom) dom.style.color = "greenyellow"; + } + if (children.length > 1) { + foundlink = true; + } + if (mark.type.name === "link" && children.length === 1) { + // let del = document.createElement("button"); + // del.textContent = "X"; + // del.style.color = "red"; + // del.style.height = "10px"; + // del.style.width = "10px"; + // del.style.marginLeft = "5px"; + // del.onclick = this.deleteLink; + // this.extras.appendChild(del); + let del = this.deleteLinkItem().render(this.view).dom; + this.extras.appendChild(del); + foundlink = true; + } + }); + if (!foundlink) { + if (children.length > 1) { + this.extras.removeChild(children[1]); + } + } + } //finds all active marks on selection in given group activeMarksOnSelection(markGroup: MarkType[]) { //current selection - let { empty, ranges } = this.view.state.selection as TextSelection; + let { empty, ranges, $to } = this.view.state.selection as TextSelection; let state = this.view.state; let dispatch = this.view.dispatch; let activeMarks: MarkType[]; @@ -686,6 +919,9 @@ export class TooltipTextMenu { } return false; }); + + const refnode = this.reference_node($to); + this._activeMarks = refnode.marks; } else { const pos = this.view.state.selection.$from; @@ -696,9 +932,7 @@ export class TooltipTextMenu { else { return []; } - this._activeMarks = ref_node.marks; - activeMarks = markGroup.filter(mark_type => { if (dispatch) { let mark = state.schema.mark(mark_type); @@ -717,12 +951,12 @@ export class TooltipTextMenu { reference_node(pos: ResolvedPos<any>): ProsNode { let ref_node: ProsNode = this.view.state.doc; - if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { - ref_node = pos.nodeAfter; - } - else if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) { + if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) { ref_node = pos.nodeBefore; } + else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { + ref_node = pos.nodeAfter; + } else if (pos.pos > 0) { let skip = false; for (let i: number = pos.pos - 1; i > 0; i--) { @@ -735,10 +969,13 @@ export class TooltipTextMenu { }); } } + if (!ref_node.isLeaf) { + ref_node = ref_node.child(0); + } return ref_node; } destroy() { - this.tooltip.remove(); + this.wrapper.remove(); } } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index a6020bd3f..59d120974 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -62,7 +62,8 @@ export default class KeyManager { private unmodified = action((keyname: string, e: KeyboardEvent) => { switch (keyname) { case "escape": - if (MainView.Instance.isPointerDown) { + let main = MainView.Instance; + if (main.isPointerDown) { DragManager.AbortDrag(); } else { if (CollectionDockingView.Instance.HasFullScreen()) { @@ -71,10 +72,12 @@ export default class KeyManager { SelectionManager.DeselectAll(); } } - MainView.Instance.toggleColorPicker(true); + main.toggleColorPicker(true); SelectionManager.DeselectAll(); DictationManager.Instance.stop(); - MainView.Instance.dictationOverlayVisible = false; + main.dictationOverlayVisible = false; + main.dictationSuccess = undefined; + main.overlayTimeout && clearTimeout(main.overlayTimeout); break; case "delete": case "backspace": @@ -103,8 +106,8 @@ export default class KeyManager { }); private shift = async (keyname: string) => { - let stopPropagation = true; - let preventDefault = true; + let stopPropagation = false; + let preventDefault = false; switch (keyname) { case " ": @@ -120,10 +123,12 @@ export default class KeyManager { command = command.toLowerCase(); main.dictatedPhrase = command; main.dictationSuccess = await manager.execute(command); - setTimeout(() => { + main.overlayTimeout = setTimeout(() => { main.dictationOverlayVisible = false; main.dictationSuccess = undefined; }, 3000); + stopPropagation = true; + preventDefault = true; } return { diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index 8e2d7be85..72eb956e3 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -12,6 +12,7 @@ import "./MainOverlayTextBox.scss"; import { FormattedTextBox } from './nodes/FormattedTextBox'; interface MainOverlayTextBoxProps { + firstinstance?: boolean; } @observer diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index d6449cffc..4a5e4a3d1 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowDown, faCaretUp, faLongArrowAltRight, faCloudUploadAlt, faArrowUp, faClone, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat, faBolt } from '@fortawesome/free-solid-svg-icons'; +import { faArrowDown, faCloudUploadAlt, faArrowUp, faClone, faCheck, faPlay, faPause, faCaretUp, faLongArrowAltRight, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat, faBolt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; @@ -53,6 +53,8 @@ export class MainView extends React.Component { @observable private dictationDisplayState = false; @observable private dictationListeningState = false; + public overlayTimeout: NodeJS.Timeout | undefined; + @computed private get mainContainer(): Opt<Doc> { return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc)); } @@ -163,6 +165,8 @@ export class MainView extends React.Component { library.add(faFilm); library.add(faMusic); library.add(faTree); + library.add(faPlay); + library.add(faPause); library.add(faClone); library.add(faCut); library.add(faCommentAlt); @@ -419,7 +423,7 @@ export class MainView extends React.Component { let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; // let addDockingNode = action(() => Docs.Create.StandardCollectionDockingDocument([{ doc: addColNode(), initialWidth: 200 }], { width: 200, height: 200, title: "a nested docking freeform collection" })); - let addSchemaNode = action(() => Docs.Create.SchemaDocument([new SchemaHeaderField("title")], [], { width: 200, height: 200, title: "a schema collection" })); + let addSchemaNode = action(() => Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], [], { width: 200, height: 200, title: "a schema collection" })); //let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" })); // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" })); let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" })); @@ -427,12 +431,15 @@ export class MainView extends React.Component { let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" })); let addButtonDocument = action(() => Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })); let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })); + let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; + let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" })); let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [ [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument], // [React.createRef<HTMLDivElement>(), "clone", "Add Docking Frame", addDockingNode], [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], + [React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher] ]; if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]); @@ -491,6 +498,7 @@ export class MainView extends React.Component { @observable isSearchVisible = false; @action toggleSearch = () => { + // console.log("search toggling") this.isSearchVisible = !this.isSearchVisible; } @@ -519,7 +527,7 @@ export class MainView extends React.Component { {this.nodesMenu()} {this.miscButtons} <PDFMenu /> - <MainOverlayTextBox /> + <MainOverlayTextBox firstinstance={true} /> <OverlayView /> </div > ); diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx new file mode 100644 index 000000000..13e4b88f7 --- /dev/null +++ b/src/client/views/SearchItem.tsx @@ -0,0 +1,69 @@ +import React = require("react"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Doc } from "../../new_fields/Doc"; +import { DocumentManager } from "../util/DocumentManager"; +import { SetupDrag } from "../util/DragManager"; + + +export interface SearchProps { + doc: Doc; +} + +library.add(faCaretUp); +library.add(faObjectGroup); +library.add(faStickyNote); +library.add(faFilePdf); +library.add(faFilm); + +export class SearchItem extends React.Component<SearchProps> { + + onClick = () => { + DocumentManager.Instance.jumpToDocument(this.props.doc, false); + } + + //needs help + // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; } + + + public static DocumentIcon(layout: string) { + let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : + layout.indexOf("ImageBox") !== -1 ? faImage : + layout.indexOf("Formatted") !== -1 ? faStickyNote : + layout.indexOf("Video") !== -1 ? faFilm : + layout.indexOf("Collection") !== -1 ? faObjectGroup : + faCaretUp; + return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; + } + onPointerEnter = (e: React.PointerEvent) => { + this.props.doc.libraryBrush = true; + Doc.SetOnPrototype(this.props.doc, "protoBrush", true); + } + onPointerLeave = (e: React.PointerEvent) => { + this.props.doc.libraryBrush = false; + Doc.SetOnPrototype(this.props.doc, "protoBrush", false); + } + + collectionRef = React.createRef<HTMLDivElement>(); + startDocDrag = () => { + let doc = this.props.doc; + const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); + if (isProto) { + return Doc.MakeDelegate(doc); + } else { + return Doc.MakeAlias(doc); + } + } + render() { + return ( + <div className="search-item" ref={this.collectionRef} id="result" + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} + onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} > + <div className="search-title" id="result" >title: {this.props.doc.title}</div> + {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */} + {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index fdf0896ac..4ff65b277 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -26,6 +26,7 @@ import { faExpand } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { KeyCodes } from "../../northstar/utils/KeyCodes"; +import { undoBatch } from "../../util/UndoManager"; library.add(faExpand); @@ -71,6 +72,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { document.removeEventListener("keydown", this.onKeyDown); this._isEditing = true; this.props.setIsEditing(true); + } } @@ -87,11 +89,15 @@ export class CollectionSchemaCell extends React.Component<CellProps> { this.props.changeFocusedCellByIndex(this.props.row, this.props.col); this.props.setPreviewDoc(this.props.rowProps.original); + // this._isEditing = true; + // this.props.setIsEditing(true); + let field = this.props.rowProps.original[this.props.rowProps.column.id!]; let doc = FieldValue(Cast(field, Doc)); if (typeof field === "object" && doc) this.props.setPreviewDoc(doc); } + @undoBatch applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) }); if (!res.success) return false; @@ -108,7 +114,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { this._document[fieldKey] = de.data.draggedDocuments[0]; } else { - let coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title")], de.data.draggedDocuments, {}); + let coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.data.draggedDocuments, {}); this._document[fieldKey] = coll; } e.stopPropagation(); @@ -122,17 +128,17 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } } - expandDoc = (e: React.PointerEvent) => { - let field = this.props.rowProps.original[this.props.rowProps.column.id as string]; - let doc = FieldValue(Cast(field, Doc)); + // expandDoc = (e: React.PointerEvent) => { + // let field = this.props.rowProps.original[this.props.rowProps.column.id as string]; + // let doc = FieldValue(Cast(field, Doc)); - console.log("Expanding doc", StrCast(doc!.title)); - this.props.setPreviewDoc(doc!); + // console.log("Expanding doc", StrCast(doc!.title)); + // this.props.setPreviewDoc(doc!); - // this.props.changeFocusedCellByIndex(this.props.row, this.props.col); + // // this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - e.stopPropagation(); - } + // e.stopPropagation(); + // } renderCellWithType(type: string | undefined) { let dragRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -285,7 +291,7 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { this._isChecked = e.target.checked; let script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } }); if (script.compiled) { - this.applyToDoc(this._document, script.run); + this.applyToDoc(this._document, this.props.row, this.props.col, script.run); } } diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index 387107c55..dfd65770e 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { action, computed, observable, trace, untracked } from "mobx"; import { observer } from "mobx-react"; import "./CollectionSchemaView.scss"; -import { faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn } from '@fortawesome/free-solid-svg-icons'; +import { faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faSortAmountDown, faSortAmountUp, faTimes } from '@fortawesome/free-solid-svg-icons'; import { library, IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Flyout, anchorPoints } from "../DocumentDecorations"; @@ -10,9 +10,10 @@ import { ColumnType } from "./CollectionSchemaView"; import { emptyFunction } from "../../../Utils"; import { contains } from "typescript-collections/dist/lib/arrays"; import { faFile } from "@fortawesome/free-regular-svg-icons"; -import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { SchemaHeaderField, RandomPastel, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField"; +import { undoBatch } from "../../util/UndoManager"; -library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faFile); +library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faFile, faSortAmountDown, faSortAmountUp, faTimes); export interface HeaderProps { keyValue: SchemaHeaderField; @@ -23,23 +24,24 @@ export interface HeaderProps { onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; setIsEditing: (isEditing: boolean) => void; deleteColumn: (column: string) => void; - setColumnType: (key: string, type: ColumnType) => void; - setColumnSort: (key: string, desc: boolean) => void; - removeColumnSort: (key: string) => void; + setColumnType: (column: SchemaHeaderField, type: ColumnType) => void; + setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void; + setColumnColor: (column: SchemaHeaderField, color: string) => void; + } export class CollectionSchemaHeader extends React.Component<HeaderProps> { render() { let icon: IconProp = this.props.keyType === ColumnType.Number ? "hashtag" : this.props.keyType === ColumnType.String ? "font" : this.props.keyType === ColumnType.Boolean ? "check-square" : this.props.keyType === ColumnType.Doc ? "file" : "align-justify"; - return ( <div className="collectionSchemaView-header" style={{ background: this.props.keyValue.color }}> <CollectionSchemaColumnMenu - keyValue={this.props.keyValue.heading} + columnField={this.props.keyValue} + // keyValue={this.props.keyValue.heading} possibleKeys={this.props.possibleKeys} existingKeys={this.props.existingKeys} - keyType={this.props.keyType} + // keyType={this.props.keyType} typeConst={this.props.typeConst} menuButtonContent={<div><FontAwesomeIcon icon={icon} size="sm" />{this.props.keyValue.heading}</div>} addNew={false} @@ -49,7 +51,7 @@ export class CollectionSchemaHeader extends React.Component<HeaderProps> { onlyShowOptions={false} setColumnType={this.props.setColumnType} setColumnSort={this.props.setColumnSort} - removeColumnSort={this.props.removeColumnSort} + setColumnColor={this.props.setColumnColor} /> </div> ); @@ -70,13 +72,12 @@ export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHe } } - - export interface ColumnMenuProps { - keyValue: string; + columnField: SchemaHeaderField; + // keyValue: string; possibleKeys: string[]; existingKeys: string[]; - keyType: ColumnType; + // keyType: ColumnType; typeConst: boolean; menuButtonContent: JSX.Element; addNew: boolean; @@ -84,10 +85,10 @@ export interface ColumnMenuProps { setIsEditing: (isEditing: boolean) => void; deleteColumn: (column: string) => void; onlyShowOptions: boolean; - setColumnType: (key: string, type: ColumnType) => void; - setColumnSort: (key: string, desc: boolean) => void; - removeColumnSort: (key: string) => void; + setColumnType: (column: SchemaHeaderField, type: ColumnType) => void; + setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void; anchorPoint?: any; + setColumnColor: (column: SchemaHeaderField, color: string) => void; } @observer export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> { @@ -116,10 +117,16 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> this.props.setIsEditing(this._isOpen); } - setColumnType = (oldKey: string, newKey: string, addnew: boolean) => { - let typeStr = newKey as keyof typeof ColumnType; - let type = ColumnType[typeStr]; - this.props.setColumnType(this.props.keyValue, type); + changeColumnType = (type: ColumnType): void => { + this.props.setColumnType(this.props.columnField, type); + } + + changeColumnSort = (desc: boolean | undefined): void => { + this.props.setColumnSort(this.props.columnField, desc); + } + + changeColumnColor = (color: string): void => { + this.props.setColumnColor(this.props.columnField, color); } @action @@ -129,78 +136,82 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> } } - changeColumnColor = (color: string): void => { - - } - renderTypes = () => { if (this.props.typeConst) return <></>; + + let type = this.props.columnField.type; return ( <div className="collectionSchema-headerMenu-group"> <label>Column type:</label> <div className="columnMenu-types"> - <button title="Any" className={this.props.keyType === ColumnType.Any ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Any)}> + <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Any)}> <FontAwesomeIcon icon={"align-justify"} size="sm" /> - </button> - <button title="Number" className={this.props.keyType === ColumnType.Number ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Number)}> + Any + </div> + <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Number)}> <FontAwesomeIcon icon={"hashtag"} size="sm" /> - </button> - <button title="String" className={this.props.keyType === ColumnType.String ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.String)}> + Number + </div> + <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.String)}> <FontAwesomeIcon icon={"font"} size="sm" /> - </button> - <button title="Checkbox" className={this.props.keyType === ColumnType.Boolean ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Boolean)}> + Text + </div> + <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Boolean)}> <FontAwesomeIcon icon={"check-square"} size="sm" /> - </button> - <button title="Document" className={this.props.keyType === ColumnType.Doc ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Doc)}> + Checkbox + </div> + <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Doc)}> <FontAwesomeIcon icon={"file"} size="sm" /> - </button> + Document + </div> </div> - </div> + </div > ); } renderSorting = () => { + let sort = this.props.columnField.desc; return ( <div className="collectionSchema-headerMenu-group"> <label>Sort by:</label> <div className="columnMenu-sort"> - <div className="columnMenu-option" onClick={() => this.props.setColumnSort(this.props.keyValue, false)}>Sort ascending</div> - <div className="columnMenu-option" onClick={() => this.props.setColumnSort(this.props.keyValue, true)}>Sort descending</div> - <div className="columnMenu-option" onClick={() => this.props.removeColumnSort(this.props.keyValue)}>Clear sorting</div> + <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.changeColumnSort(true)}> + <FontAwesomeIcon icon="sort-amount-down" size="sm" /> + Sort descending + </div> + <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.changeColumnSort(false)}> + <FontAwesomeIcon icon="sort-amount-up" size="sm" /> + Sort ascending + </div> + <div className="columnMenu-option" onClick={() => this.changeColumnSort(undefined)}> + <FontAwesomeIcon icon="times" size="sm" /> + Clear sorting + </div> </div> </div> ); } renderColors = () => { + let selected = this.props.columnField.color; + + let pink = PastelSchemaPalette.get("pink2"); + let purple = PastelSchemaPalette.get("purple2"); + let blue = PastelSchemaPalette.get("bluegreen1"); + let yellow = PastelSchemaPalette.get("yellow4"); + let red = PastelSchemaPalette.get("red2"); + let gray = "#f1efeb"; + return ( <div className="collectionSchema-headerMenu-group"> <label>Color:</label> <div className="columnMenu-colors"> - <input type="radio" name="column-color" id="pink" value="#FFB4E8" onClick={() => this.changeColumnColor("#FFB4E8")} /> - <label htmlFor="pink"> - <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> - </label> - <input type="radio" name="column-color" id="purple" value="#b28dff" onClick={() => this.changeColumnColor("#b28dff")} /> - <label htmlFor="purple"> - <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> - </label> - <input type="radio" name="column-color" id="blue" value="#afcbff" onClick={() => this.changeColumnColor("#afcbff")} /> - <label htmlFor="blue"> - <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> - </label> - <input type="radio" name="column-color" id="yellow" value="#f3ffe3" onClick={() => this.changeColumnColor("#f3ffe3")} /> - <label htmlFor="yellow"> - <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> - </label> - <input type="radio" name="column-color" id="red" value="#ffc9de" onClick={() => this.changeColumnColor("#ffc9de")} /> - <label htmlFor="red"> - <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> - </label> - <input type="radio" name="column=color" id="none" value="#f1efeb" onClick={() => this.changeColumnColor("#f1efeb")} /> - <label htmlFor="none"> - <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> - </label> + <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div> + <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div> + <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div> + <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div> + <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div> + <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div> </div> </div> ); @@ -209,10 +220,10 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> renderContent = () => { return ( <div className="collectionSchema-header-menuOptions"> - <label>Key:</label> <div className="collectionSchema-headerMenu-group"> + <label>Key:</label> <KeysDropdown - keyValue={this.props.keyValue} + keyValue={this.props.columnField.heading} possibleKeys={this.props.possibleKeys} existingKeys={this.props.existingKeys} canAddNew={true} @@ -227,7 +238,7 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> {this.renderSorting()} {this.renderColors()} <div className="collectionSchema-headerMenu-group"> - <button onClick={() => this.props.deleteColumn(this.props.keyValue)}>Delete Column</button> + <button onClick={() => this.props.deleteColumn(this.props.columnField.heading)}>Delete Column</button> </div> </> } @@ -259,9 +270,10 @@ interface KeysDropdownProps { @observer class KeysDropdown extends React.Component<KeysDropdownProps> { @observable private _key: string = this.props.keyValue; - @observable private _searchTerm: string = ""; + @observable private _searchTerm: string = this.props.keyValue; @observable private _isOpen: boolean = false; @observable private _canClose: boolean = true; + @observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef(); @action setSearchTerm = (value: string): void => { this._searchTerm = value; }; @action setKey = (key: string): void => { this._key = key; }; @@ -275,6 +287,22 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { this.props.setIsEditing(false); } + @undoBatch + @action + onKeyDown = (e: React.KeyboardEvent): void => { + if (e.key === "Enter") { + let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); + let exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 || + this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1; + + if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { + this.onSelect(this._searchTerm); + } else { + this._searchTerm = this._key; + } + } + } + onChange = (val: string): void => { this.setSearchTerm(val); } @@ -327,7 +355,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { render() { return ( <div className="keys-dropdown"> - <input className="keys-search" type="text" value={this._searchTerm} placeholder="Search for or create a new key" + <input className="keys-search" ref={this._inputRef} type="text" value={this._searchTerm} placeholder="Column key" onKeyDown={this.onKeyDown} onChange={e => this.onChange(e.target.value)} onFocus={this.onFocus} onBlur={this.onBlur}></input> <div className="keys-options-wrapper" onPointerEnter={this.onPointerEnter} onPointerOut={this.onPointerOut}> {this.renderOptions()} diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index 7342ede7a..ec40043cc 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -13,6 +13,7 @@ import { faGripVertical, faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { DocumentManager } from "../../util/DocumentManager"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { undoBatch } from "../../util/UndoManager"; library.add(faGripVertical, faTrash); @@ -26,6 +27,9 @@ export interface MovableColumnProps { export class MovableColumn extends React.Component<MovableColumnProps> { private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _colDropDisposer?: DragManager.DragDropDisposer; + private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; + private _sensitivity: number = 16; + private _dragRef: React.RefObject<HTMLDivElement> = React.createRef(); onPointerEnter = (e: React.PointerEvent): void => { if (e.buttons === 1 && SelectionManager.GetIsDragging()) { @@ -36,6 +40,7 @@ export class MovableColumn extends React.Component<MovableColumnProps> { onPointerLeave = (e: React.PointerEvent): void => { this._header!.current!.className = "collectionSchema-col-wrapper"; document.removeEventListener("pointermove", this.onDragMove, true); + document.removeEventListener("pointermove", this.onPointerMove); } onDragMove = (e: PointerEvent): void => { let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); @@ -68,7 +73,7 @@ export class MovableColumn extends React.Component<MovableColumnProps> { return false; } - setupDrag(ref: React.RefObject<HTMLElement>) { + onPointerMove = (e: PointerEvent) => { let onRowMove = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); @@ -76,35 +81,44 @@ export class MovableColumn extends React.Component<MovableColumnProps> { document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); let dragData = new DragManager.ColumnDragData(this.props.columnValue); - DragManager.StartColumnDrag(ref.current!, dragData, e.x, e.y); + DragManager.StartColumnDrag(this._dragRef.current!, dragData, e.x, e.y); }; let onRowUp = (): void => { document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); }; - let onItemDown = (e: React.PointerEvent) => { - if (e.button === 0) { + if (e.buttons === 1) { + let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y); + if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { + document.removeEventListener("pointermove", this.onPointerMove); e.stopPropagation(); + document.addEventListener("pointermove", onRowMove); document.addEventListener("pointerup", onRowUp); } - }; - return onItemDown; + } } - // onColDrag = (e: React.DragEvent, ref: React.RefObject<HTMLDivElement>) => { - // this.setupDrag(reference); - // } + onPointerUp = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.onPointerMove); + } + + @action + onPointerDown = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => { + this._dragRef = ref; + let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY); + this._startDragPosition = { x: dx, y: dy }; + document.addEventListener("pointermove", this.onPointerMove); + } render() { let reference = React.createRef<HTMLDivElement>(); - let onItemDown = this.setupDrag(reference); return ( <div className="collectionSchema-col" ref={this.createColDropTarget}> <div className="collectionSchema-col-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> - <div className="col-dragger" ref={reference} onPointerDown={onItemDown} > + <div className="col-dragger" ref={reference} onPointerDown={e => this.onPointerDown(e, reference)} onPointerUp={this.onPointerUp}> {this.props.columnRenderer} </div> </div> @@ -183,6 +197,7 @@ export class MovableRow extends React.Component<MovableRowProps> { ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original), icon: "file-pdf" }); } + @undoBatch @action move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => { let targetView = DocumentManager.Instance.getDocumentView(target); @@ -212,7 +227,6 @@ export class MovableRow extends React.Component<MovableRowProps> { let className = "collectionSchema-row"; if (this.props.rowFocused) className += " row-focused"; if (this.props.rowWrapped) className += " row-wrapped"; - // if (!this.props.rowWrapped) className += " row-unwrapped"; return ( <div className={className} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}> diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index b1e98b162..3c4279eea 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -9,26 +9,22 @@ position: absolute; top: 0; width: 100%; - transition: height .5s; height: 100%; - // overflow: hidden; - // overflow-x: scroll; - // border: none; - overflow: hidden; + transition: top 0.5s; + display: flex; + justify-content: space-between; + flex-wrap: nowrap; - // .collectionSchemaView-cellContents { - // height: $MAX_ROW_HEIGHT; - // img { - // width: auto; - // max-height: $MAX_ROW_HEIGHT; - // } - // } + .collectionSchemaView-tableContainer { + width: 100%; + height: 100%; + overflow: scroll; + } .collectionSchemaView-previewRegion { position: relative; background: $light-color; - float: left; height: 100%; .collectionSchemaView-previewDoc { @@ -52,7 +48,6 @@ .collectionSchemaView-dividerDragger { position: relative; - float: left; height: 100%; width: 20px; z-index: 20; @@ -60,50 +55,31 @@ top: 0; background: gray; cursor: col-resize; - // background: $main-accent; - // box-sizing: border-box; - // border-left: 1px solid $intermediate-color; - // border-right: 1px solid $intermediate-color; } } .ReactTable { width: 100%; - height: 100%; background: white; box-sizing: border-box; border: none !important; + float: none !important; .rt-table { - overflow-y: auto; - overflow-x: auto; height: 100%; display: -webkit-inline-box; - direction: ltr; + direction: ltr; + overflow: visible; } .rt-thead { - width: calc(100% - 50px); + width: calc(100% - 52px); margin-left: 50px; &.-header { - // background: $intermediate-color; - // color: $light-color; font-size: 12px; height: 30px; - // border: 1px solid $intermediate-color; box-shadow: none; - // width: calc(100% - 30px); - // margin-right: -30px; - } - - .rt-resizable-header { - padding: 0; - height: 30px; - - &:last-child { - overflow: visible; - } } .rt-resizable-header-content { @@ -115,14 +91,13 @@ padding: 0; border: solid lightgray; border-width: 0 1px; + border-bottom: 2px solid lightgray; } } .rt-th { - // max-height: $MAX_ROW_HEIGHT; font-size: 13px; text-align: center; - background-color: $light-color-secondary; &:last-child { overflow: visible; @@ -130,6 +105,7 @@ } .rt-tbody { + width: calc(100% - 2px); direction: rtl; overflow: visible; } @@ -139,45 +115,18 @@ flex: 0 1 auto; min-height: 30px; border: 0 !important; - // border: solid lightgray; - // border-width: 1px 0; - // border-left: 1px solid lightgray; - // max-height: $MAX_ROW_HEIGHT; - // for sub comp - - // &:nth-child(even) { - // background-color: $light-color; - // } - - // &:nth-child(odd) { - // background-color: $light-color-secondary; - // } - - // &:first-child { - // border-top: 1px solid $light-color-secondary !important; - // } - // &:last-child { - // border-bottom: 1px solid $light-color-secondary !important; - // } } .rt-tr { width: 100%; min-height: 30px; - // height: $MAX_ROW_HEIGHT; } .rt-td { - // border: 1px solid $light-color-secondary !important; - // border-width: 0 1px; - // border-width: 1px; - // border-right-color: $intermediate-color; - // max-height: $MAX_ROW_HEIGHT; padding: 0; font-size: 13px; text-align: center; - - // white-space: normal; + white-space: nowrap; .imageBox-cont { position: relative; @@ -198,8 +147,21 @@ } .rt-resizer { - width: 20px; - right: -10px; + width: 8px; + right: -4px; + } + + .rt-resizable-header { + padding: 0; + height: 30px; + } + + .rt-resizable-header:last-child { + overflow: visible; + + .rt-resizer { + width: 5px !important; + } } } @@ -246,11 +208,6 @@ margin-right: 4px; } } - - // div[class*="css"] { - // width: 100%; - // height: 100%; - // } } } @@ -260,16 +217,29 @@ button.add-column { .collectionSchema-header-menuOptions { color: black; - width: 175px; + width: 200px; text-align: left; .collectionSchema-headerMenu-group { - margin-bottom: 10px; + padding: 7px 0; + border-bottom: 1px solid lightgray; + + &:first-child { + padding-top : 0; + } + + &:last-child { + border: none; + text-align: center; + padding: 12px 0 0 0; + } } label { color: $main-accent; font-weight: normal; + letter-spacing: 2px; + text-transform: uppercase; } input { @@ -277,23 +247,57 @@ button.add-column { width: 100%; } + .columnMenu-option { + cursor: pointer; + padding: 3px; + background-color: white; + transition: background-color 0.2s; + + &:hover { + background-color: $light-color-secondary; + } + + &.active { + font-weight: bold; + border: 2px solid $light-color-secondary; + } + + svg { + color: gray; + margin-right: 5px; + width: 10px; + } + } + .keys-dropdown { position: relative; - max-width: 175px; + width: 100%; + + input { + border: 2px solid $light-color-secondary; + padding: 3px; + height: 28px; + font-weight: bold; + + &:focus { + font-weight: normal; + } + } .keys-options-wrapper { width: 100%; max-height: 150px; overflow-y: scroll; position: absolute; - top: 20px; + top: 28px; + box-shadow: 0 10px 16px rgba(0,0,0,0.1); .key-option { background-color: $light-color; - border: 1px solid $light-color-secondary; + border: 1px solid lightgray; padding: 2px 3px; - &:not(:last-child) { + &:not(:first-child) { border-top: 0; } @@ -304,60 +308,51 @@ button.add-column { } } - .columnMenu-types { + .columnMenu-colors { display: flex; justify-content: space-between; - - button { - border-radius: 20px; - } - } - - .columnMenu-colors { - - - input[type="radio"] { - display: none; - } + flex-wrap: wrap; .columnMenu-colorPicker { + cursor: pointer; width: 20px; height: 20px; + border-radius: 10px; + + &.active { + border: 2px solid white; + box-shadow: 0 0 0 2px lightgray; + } } } } .collectionSchema-row { - // height: $MAX_ROW_HEIGHT; height: 100%; background-color: white; - &.row-focused .rt-tr { + &.row-focused .rt-td { background-color: rgb(255, 246, 246); //$light-color-secondary; } &.row-wrapped { - white-space: normal; + .rt-td { + white-space: normal; + } } .row-dragger { display: flex; justify-content: space-around; - // height: $MAX_ROW_HEIGHT; flex: 50 0 auto; width: 50px; max-width: 50px; height: 100%; min-height: 30px; - // padding: 5px 5px 5px 0; color: lightgray; background-color: white; transition: color 0.1s ease; - // &:hover { - // color: lightgray; - // } - .row-option { // padding: 5px; cursor: pointer; @@ -373,7 +368,6 @@ button.add-column { } .collectionSchema-row-wrapper { - // max-height: $MAX_ROW_HEIGHT; &.row-above { border-top: 1px solid red; @@ -407,18 +401,22 @@ button.add-column { outline: none; } - &.focused { - // background-color: yellowgreen; - // border: 2px solid yellowgreen; - + &.editing { + padding: 0; input { outline: 0; border: none; - background-color: yellow; + background-color: rgb(255, 217, 217); + width: 100%; + height: 100%; + padding: 2px 3px; + min-height: 26px; } + } + + &.focused { &.inactive { - // border: 2px solid rgba(255, 255, 0, 0.4); border: none; } } @@ -426,7 +424,6 @@ button.add-column { p { width: 100%; height: 100%; - // word-wrap: break-word; } &:hover .collectionSchemaView-cellContents-docExpander { @@ -453,9 +450,7 @@ button.add-column { display: flex; justify-content: flex-end; padding: 0 10px; - border-bottom: 2px solid gray; - // margin-bottom: 10px; .collectionSchemaView-toolbar-item { display: flex; @@ -470,23 +465,17 @@ button.add-column { } .collectionSchemaView-table { - width: calc(100% - 7px); + width: 100%; + height: 100%; + overflow: visible; } .sub { padding: 10px 30px; - // padding-left: 80px; background-color: rgb(252, 252, 252); width: calc(100% - 50px); margin-left: 50px; - .rt-table { - overflow-x: hidden; // todo; this shouldnt be like this :(( - overflow-y: visible; - } - - // TODO fix - .row-dragger { background-color: rgb(252, 252, 252); } @@ -502,4 +491,25 @@ button.add-column { .collectionSchemaView-expander { height: 100%; + min-height: 30px; + position: relative; + color: gray; + + svg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} + +.collectionSchemaView-addRow { + color: gray; + letter-spacing: 2px; + text-transform: uppercase; + cursor: pointer; + font-size: 10.5px; + padding: 10px; + margin-left: 50px; + margin-top: 10px; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 08ab22725..4d6bf437f 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -4,10 +4,10 @@ import { faCog, faPlus, faTable, faSortUp, faSortDown } from '@fortawesome/free- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, trace, untracked } from "mobx"; import { observer } from "mobx-react"; -import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults, TableCellRenderer, Column, RowInfo } from "react-table"; +import ReactTable, { CellInfo, ComponentPropsGetterR, Column, RowInfo, ResizedChangeFunction, Resize } from "react-table"; import "react-table/react-table.css"; -import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; -import { Doc, DocListCast, DocListCastAsync, Field, FieldResult, Opt } from "../../../new_fields/Doc"; +import { emptyFunction, returnOne } from "../../../Utils"; +import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; @@ -17,28 +17,21 @@ import { Gateway } from "../../northstar/manager/Gateway"; import { SetupDrag, DragManager } from "../../util/DragManager"; import { CompileScript, ts, Transformer } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; -import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'; +import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; import { ContextMenu } from "../ContextMenu"; -import { anchorPoints, Flyout } from "../DocumentDecorations"; import '../DocumentDecorations.scss'; -import { EditableView } from "../EditableView"; import { DocumentView } from "../nodes/DocumentView"; -import { FieldView, FieldViewProps } from "../nodes/FieldView"; import { CollectionPDFView } from "./CollectionPDFView"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; import { undoBatch } from "../../util/UndoManager"; -import { timesSeries } from "async"; import { CollectionSchemaHeader, CollectionSchemaAddColumnHeader } from "./CollectionSchemaHeaders"; import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell } from "./CollectionSchemaCells"; import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; -import { SelectionManager } from "../../util/SelectionManager"; -import { DocumentManager } from "../../util/DocumentManager"; -import { ImageBox } from "../nodes/ImageBox"; import { ComputedField } from "../../../new_fields/ScriptField"; -import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField"; +import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; library.add(faCog, faPlus, faSortUp, faSortDown); @@ -51,7 +44,6 @@ export enum ColumnType { String, Boolean, Doc, - // Checkbox } // this map should be used for keys that should have a const type of value const columnTypes: Map<string, ColumnType> = new Map([ @@ -82,14 +74,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { super.CreateDropTarget(ele); } - // detectClick = (e: PointerEvent): void => { - // if (this._node && this._node.contains(e.target as Node)) { - // } else { - // this._isOpen = false; - // this.props.setIsEditing(false); - // } - // } - isFocused = (doc: Doc): boolean => { if (!this.props.isSelected()) return false; return doc === this._focusedTable; @@ -121,8 +105,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action onDividerMove = (e: PointerEvent): void => { let nativeWidth = this._mainCont!.getBoundingClientRect(); - this.props.Document.schemaPreviewWidth = Math.min(nativeWidth.right - nativeWidth.left - 40, - this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]); + let minWidth = 40; + let maxWidth = 1000; + let movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]; + let width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth; + this.props.Document.schemaPreviewWidth = width; } @action onDividerUp = (e: PointerEvent): void => { @@ -189,6 +176,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { /> </div>; } + + @undoBatch @action setPreviewScript = (script: string) => { this.previewScript = script; @@ -234,12 +223,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { render() { Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); - // if (SelectionManager.SelectedDocuments().length > 0) console.log(StrCast(SelectionManager.SelectedDocuments()[0].Document.title)); - // if (DocumentManager.Instance.getDocumentView(this.props.Document)) console.log(StrCast(this.props.Document.title), SelectionManager.IsSelected(DocumentManager.Instance.getDocumentView(this.props.Document)!)) return ( - <div className="collectionSchemaView-container" - onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}> - {this.schemaTable} + <div className="collectionSchemaView-container" style={{ height: "100%", marginTop: "0", }}> + <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}> + {this.schemaTable} + </div> {this.dividerDragger} {!this.previewWidth() ? (null) : this.previewPanel} </div> @@ -260,7 +248,6 @@ export interface SchemaTableProps { deleteDocument: (document: Doc) => boolean; moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; - // CreateDropTarget: (ele: HTMLDivElement)=> void; // super createdriotarget active: () => boolean; onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; @@ -272,24 +259,24 @@ export interface SchemaTableProps { @observer export class SchemaTable extends React.Component<SchemaTableProps> { - // private _mainCont?: HTMLDivElement; private DIVIDER_WIDTH = 4; @observable _headerIsEditing: boolean = false; @observable _cellIsEditing: boolean = false; @observable _focusedCell: { row: number, col: number } = { row: 0, col: 0 }; - @observable _sortedColumns: Map<string, { id: string, desc: boolean }> = new Map(); @observable _openCollections: Array<string> = []; - @observable _textWrappedRows: Array<string> = []; - @observable private _node: HTMLDivElement | null = null; @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); } + @computed get columns() { - console.log("columns"); return Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField), []); } + set columns(columns: SchemaHeaderField[]) { + this.props.Document.schemaColumns = new List<SchemaHeaderField>(columns); + } + @computed get childDocs() { if (this.props.childDocs) return this.props.childDocs; @@ -300,7 +287,32 @@ export class SchemaTable extends React.Component<SchemaTableProps> { let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; doc[this.props.fieldKey] = new List<Doc>(docs); } - set columns(columns: SchemaHeaderField[]) { this.props.Document.schemaColumns = new List<SchemaHeaderField>(columns); } + + @computed get textWrappedRows() { + return Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); + } + set textWrappedRows(textWrappedRows: string[]) { + this.props.Document.textwrappedSchemaRows = new List<string>(textWrappedRows); + } + + @computed get resized(): { "id": string, "value": number }[] { + return this.columns.reduce((resized, shf) => { + if (shf.width > -1) { + resized.push({ "id": shf.heading, "value": shf.width }); + } + return resized; + }, [] as { "id": string, "value": number }[]); + } + + @computed get sorted(): { "id": string, "desc": boolean }[] { + return this.columns.reduce((sorted, shf) => { + if (shf.desc) { + sorted.push({ "id": shf.heading, "desc": shf.desc }); + } + return sorted; + }, [] as { "id": string, "desc": boolean }[]); + } + @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @computed get tableColumns(): Column<Doc>[] { let possibleKeys = this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); @@ -310,8 +322,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { let focusedCol = this._focusedCell.col; let isEditable = !this._headerIsEditing;// && this.props.isSelected(); - // let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - // let children = DocListCast(cdoc[this.props.fieldKey]); let children = this.childDocs; if (children.reduce((found, doc) => found || doc.type === "collection", false)) { @@ -344,7 +354,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { deleteColumn={this.deleteColumn} setColumnType={this.setColumnType} setColumnSort={this.setColumnSort} - removeColumnSort={this.removeColumnSort} + setColumnColor={this.setColumnColor} />; return { @@ -399,25 +409,11 @@ export class SchemaTable extends React.Component<SchemaTableProps> { return columns; } - // onHeaderDrag = (columnName: string) => { - // let schemaDoc = Cast(this.props.Document.schemaDoc, Doc); - // if (schemaDoc instanceof Doc) { - // let columnDocs = DocListCast(schemaDoc.data); - // if (columnDocs) { - // let ddoc = columnDocs.find(doc => doc.title === columnName); - // if (ddoc) { - // return ddoc; - // } - // } - // } - // return this.props.Document; - // } constructor(props: SchemaTableProps) { super(props); // convert old schema columns (list of strings) into new schema columns (list of schema header fields) let oldSchemaColumns = Cast(this.props.Document.schemaColumns, listSpec("string"), []); if (oldSchemaColumns && oldSchemaColumns.length && typeof oldSchemaColumns[0] !== "object") { - console.log("REMAKING COLUMNs"); let newSchemaColumns = oldSchemaColumns.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); this.props.Document.schemaColumns = new List<SchemaHeaderField>(newSchemaColumns); } @@ -436,8 +432,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } tableRemoveDoc = (document: Doc): boolean => { - // let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - // let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); + let children = this.childDocs; if (children.indexOf(document) !== -1) { children.splice(children.indexOf(document), 1); @@ -456,11 +451,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { ScreenToLocalTransform: this.props.ScreenToLocalTransform, addDoc: this.tableAddDoc, removeDoc: this.tableRemoveDoc, - // removeDoc: this.props.deleteDocument, rowInfo, rowFocused: !this._headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document), - textWrapRow: this.textWrapRow, - rowWrapped: this._textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1 + textWrapRow: this.toggleTextWrapRow, + rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1 }; } @@ -471,9 +465,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> { let row = rowInfo.index; //@ts-ignore let col = this.columns.map(c => c.heading).indexOf(column!.id); - // let col = column ? this.columns.indexOf(column!) : -1; let isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document); - // let column = this.columns.indexOf(column.id!); + let isEditing = this.props.isFocused(this.props.Document) && this._cellIsEditing; + // TODO: editing border doesn't work :( return { style: { border: !this._headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb" @@ -481,19 +475,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { }; } - // private createTarget = (ele: HTMLDivElement) => { - // this._mainCont = ele; - // this.props.CreateDropTarget(ele); - // } - - // detectClick = (e: PointerEvent): void => { - // if (this._node && this._node.contains(e.target as Node)) { - // } else { - // this._isOpen = false; - // this.props.setIsEditing(false); - // } - // } - @action onExpandCollection = (collection: Doc): void => { this._openCollections.push(collection[Id]); @@ -533,8 +514,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { let direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : ""; this.changeFocusedCellByDirection(direction); - // let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - // let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); let children = this.childDocs; const pdoc = FieldValue(children[this._focusedCell.row]); pdoc && this.props.setPreviewDoc(pdoc); @@ -543,8 +522,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @action changeFocusedCellByDirection = (direction: string): void => { - // let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - // let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); let children = this.childDocs; switch (direction) { case "tab": @@ -569,81 +546,77 @@ export class SchemaTable extends React.Component<SchemaTableProps> { this._focusedCell = { row: this._focusedCell.row + 1 === children.length ? this._focusedCell.row : this._focusedCell.row + 1, col: this._focusedCell.col }; break; } - // const pdoc = FieldValue(children[this._focusedCell.row]); - // pdoc && this.props.setPreviewDoc(pdoc); } @action changeFocusedCellByIndex = (row: number, col: number): void => { - // let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - // let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); - this._focusedCell = { row: row, col: col }; this.props.setFocused(this.props.Document); - - // const fdoc = FieldValue(children[this._focusedCell.row]); - // fdoc && this.props.setPreviewDoc(fdoc); } + @undoBatch createRow = () => { - // let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - // let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); let children = this.childDocs; let newDoc = Docs.Create.TextDocument({ width: 100, height: 30 }); let proto = Doc.GetProto(newDoc); proto.title = ""; children.push(newDoc); + this.childDocs = children; } + @undoBatch @action createColumn = () => { let index = 0; - let found = this.columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1; + let columns = this.columns; + let found = columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1; if (!found) { - console.log("create column found"); - this.columns.push(new SchemaHeaderField("New field", "#f1efeb")); + columns.push(new SchemaHeaderField("New field", "#f1efeb")); + this.columns = columns; return; } while (found) { index++; - found = this.columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1; + found = columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1; } - console.log("create column new"); - this.columns.push(new SchemaHeaderField("New field (" + index + ")", "#f1efeb")); + columns.push(new SchemaHeaderField("New field (" + index + ")", "#f1efeb")); + this.columns = columns; } + @undoBatch @action deleteColumn = (key: string) => { - console.log("deleting columnnn"); - let list = Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField)); - if (list === undefined) { - console.log("delete column"); - this.props.Document.schemaColumns = list = new List<SchemaHeaderField>([]); + let columns = this.columns; + if (columns === undefined) { + this.columns = new List<SchemaHeaderField>([]); } else { - const index = list.map(c => c.heading).indexOf(key); + const index = columns.map(c => c.heading).indexOf(key); if (index > -1) { - list.splice(index, 1); + columns.splice(index, 1); + this.columns = columns; } } } + @undoBatch @action changeColumns = (oldKey: string, newKey: string, addNew: boolean) => { - console.log("changingin columnsdfhs"); - let list = Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField)); - if (list === undefined) { - console.log("change columns new"); - this.props.Document.schemaColumns = list = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, "f1efeb")]); + let columns = this.columns; + if (columns === undefined) { + this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, "f1efeb")]); } else { - console.log("change column"); if (addNew) { - this.columns.push(new SchemaHeaderField(newKey, "f1efeb")); + columns.push(new SchemaHeaderField(newKey, "f1efeb")); + this.columns = columns; } else { - const index = list.map(c => c.heading).indexOf(oldKey); + const index = columns.map(c => c.heading).indexOf(oldKey); if (index > -1) { - list[index] = new SchemaHeaderField(newKey, "f1efeb"); + let column = columns[index]; + column.setHeading(newKey); + columns[index] = column; + this.columns = columns; } } } @@ -667,16 +640,37 @@ export class SchemaTable extends React.Component<SchemaTableProps> { return NumCast(typesDoc[column.heading]); } - setColumnType = (key: string, type: ColumnType): void => { - if (columnTypes.get(key)) return; - const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc)); - if (!typesDoc) { - let newTypesDoc = new Doc(); - newTypesDoc[key] = type; - this.props.Document.schemaColumnTypes = newTypesDoc; - return; - } else { - typesDoc[key] = type; + @undoBatch + setColumnType = (columnField: SchemaHeaderField, type: ColumnType): void => { + if (columnTypes.get(columnField.heading)) return; + + let columns = this.columns; + let index = columns.indexOf(columnField); + if (index > -1) { + columnField.setType(NumCast(type)); + columns[index] = columnField; + this.columns = columns; + } + + // const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc)); + // if (!typesDoc) { + // let newTypesDoc = new Doc(); + // newTypesDoc[key] = type; + // this.props.Document.schemaColumnTypes = newTypesDoc; + // return; + // } else { + // typesDoc[key] = type; + // } + } + + @undoBatch + setColumnColor = (columnField: SchemaHeaderField, color: string): void => { + let columns = this.columns; + let index = columns.indexOf(columnField); + if (index > -1) { + columnField.setColor(color); + columns[index] = columnField; + this.columns = columns; // need to set the columns to trigger rerender } } @@ -685,6 +679,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { this.columns = columns; } + @undoBatch reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => { let columns = [...columnsValues]; let oldIndex = columns.indexOf(toMove); @@ -694,23 +689,22 @@ export class SchemaTable extends React.Component<SchemaTableProps> { if (oldIndex === newIndex) return; columns.splice(newIndex, 0, columns.splice(oldIndex, 1)[0]); - this.setColumns(columns); - } - - @action - setColumnSort = (column: string, descending: boolean) => { - this._sortedColumns.set(column, { id: column, desc: descending }); + this.columns = columns; } + @undoBatch @action - removeColumnSort = (column: string) => { - this._sortedColumns.delete(column); + setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => { + let columns = this.columns; + let index = columns.findIndex(c => c.heading === columnField.heading); + let column = columns[index]; + column.setDesc(descending); + columns[index] = column; + this.columns = columns; } get documentKeys() { - const docs = DocListCast(this.props.Document[this.props.fieldKey]); - - // let docs = this.childDocs; + let docs = this.childDocs; let keys: { [key: string]: boolean } = {}; // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be @@ -725,33 +719,31 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @action - textWrapRow = (doc: Doc): void => { - let index = this._textWrappedRows.findIndex(id => doc[Id] === id); + toggleTextWrapRow = (doc: Doc): void => { + let textWrapped = this.textWrappedRows; + let index = textWrapped.findIndex(id => doc[Id] === id); + if (index > -1) { - this._textWrappedRows.splice(index, 1); + textWrapped.splice(index, 1); } else { - this._textWrappedRows.push(doc[Id]); + textWrapped.push(doc[Id]); } + this.textWrappedRows = textWrapped; } @computed get reactTable() { - - // let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - // let children = DocListCast(cdoc[this.props.fieldKey]); let children = this.childDocs; - - let previewWidth = this.previewWidth(); // + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1; let hasCollectionChild = children.reduce((found, doc) => found || doc.type === "collection", false); let expandedRowsList = this._openCollections.map(col => children.findIndex(doc => doc[Id] === col).toString()); let expanded = {}; //@ts-ignore expandedRowsList.forEach(row => expanded[row] = true); - console.log(...[...this._textWrappedRows]); // TODO: get component to rerender on text wrap change without needign to console.log :(((( + console.log("text wrapped rows", ...[...this.textWrappedRows]); // TODO: get component to rerender on text wrap change without needign to console.log :(((( return <ReactTable - style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} + style={{ position: "relative" }} data={children} page={0} pageSize={children.length} @@ -761,12 +753,13 @@ export class SchemaTable extends React.Component<SchemaTableProps> { getTdProps={this.getTdProps} sortable={false} TrComponent={MovableRow} - sorted={Array.from(this._sortedColumns.values())} + sorted={this.sorted} expanded={expanded} + resized={this.resized} + onResizedChange={this.onResizedChange} SubComponent={hasCollectionChild ? row => { if (row.original.type === "collection") { - // let childDocs = DocListCast(row.original[this.props.fieldKey]); return <div className="sub"><SchemaTable {...this.props} Document={row.original} childDocs={undefined} /></div>; } } @@ -775,6 +768,17 @@ export class SchemaTable extends React.Component<SchemaTableProps> { />; } + onResizedChange = (newResized: Resize[], event: any) => { + let columns = this.columns; + newResized.forEach(resized => { + let index = columns.findIndex(c => c.heading === resized.id); + let column = columns[index]; + column.setWidth(resized.value); + columns[index] = column; + }); + this.columns = columns; + } + onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" }); @@ -786,8 +790,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { let csv: string = this.columns.reduce((val, col) => val + col + ",", ""); csv = csv.substr(0, csv.length - 1) + "\n"; let self = this; - let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - // let children = DocListCast(cdoc[this.props.fieldKey]); this.childDocs.map(doc => { csv += self.columns.reduce((val, col) => val + (doc[col.heading] ? doc[col.heading]!.toString() : "0") + ",", ""); csv = csv.substr(0, csv.length - 1) + "\n"; @@ -805,11 +807,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } getField = (row: number, col?: number) => { - // const docs = DocListCast(this.props.Document[this.props.fieldKey]); - - let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - const docs = DocListCast(cdoc[this.props.fieldKey]); - // let docs = this.childDocs; + let docs = this.childDocs; row = row % docs.length; while (row < 0) row += docs.length; @@ -881,13 +879,11 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } render() { - // if (SelectionManager.SelectedDocuments().length > 0) console.log(StrCast(SelectionManager.SelectedDocuments()[0].Document.title)); - // if (DocumentManager.Instance.getDocumentView(this.props.Document)) console.log(StrCast(this.props.Document.title), SelectionManager.IsSelected(DocumentManager.Instance.getDocumentView(this.props.Document)!)) return ( <div className="collectionSchemaView-table" onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > {this.reactTable} - <button onClick={() => this.createRow()}>new row</button> + <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> </div> ); } diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 0cb01dc9d..14c7f5edd 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -79,10 +79,10 @@ .collectionStackingView-sectionHeader { text-align: center; - margin-left: 5px; - margin-right: 5px; + margin-left: 2px; + margin-right: 2px; margin-top: 10px; - overflow: hidden; + // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong .editableView-input { color: black; @@ -125,6 +125,43 @@ } } + .collectionStackingView-sectionColor { + position: absolute; + left: 0; + top: 0; + height: 100%; + + [class*="css"] { + max-width: 102px; + } + + .collectionStackingView-sectionColorButton { + height: 35px; + } + + .collectionStackingView-colorPicker { + width: 78px; + + .colorOptions { + display: flex; + flex-wrap: wrap; + } + + .colorPicker { + cursor: pointer; + width: 20px; + height: 20px; + border-radius: 10px; + margin: 3px; + + &.active { + border: 2px solid white; + box-shadow: 0 0 0 2px lightgray; + } + } + } + } + .collectionStackingView-sectionDelete { position: absolute; right: 0; @@ -184,4 +221,52 @@ letter-spacing: 2px; height: fit-content; } + + .rc-switch { + position: absolute; + display: inline-block; + bottom: 15px; + right: 15px; + width: 125px; + height: 54px; + border-radius: 40px 40px; + } + + .rc-switch:after { + position: absolute; + width: 36px; + height: 36px; + left: 7px; + top: 9px; + border-radius: 50% 50%; + background-color: #fff; + content: " "; + cursor: pointer; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26); + -webkit-transform: scale(1); + transform: scale(1); + transition: left 0.3s cubic-bezier(0.35, 0, 0.25, 1); + -webkit-animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1); + animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1); + -webkit-animation-duration: 0.3s; + animation-duration: 0.3s; + } + + .rc-switch-checked:after { + left: 80px; + } + + .rc-switch-inner { + color: #fff; + font-size: 22px; + position: absolute; + left: 50px; + top: 14px; + } + + .rc-switch-checked .rc-switch-inner { + left: 17px; + } + + }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index bcf3a85d7..5cfcc3d4b 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -20,6 +20,7 @@ import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeade import { List } from "../../../new_fields/List"; import { EditableView } from "../EditableView"; import { CollectionViewProps } from "./CollectionBaseView"; +import Switch from 'rc-switch'; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { @@ -248,7 +249,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { docList={docList} parent={this} type={type} - createDropTarget={this.createDropTarget} />; + createDropTarget={this.createDropTarget} + screenToLocalTransform={this.props.ScreenToLocalTransform} + />; + } @action @@ -269,6 +273,14 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1; } + onToggle = (checked: Boolean) => { + if (checked) { + this.props.CollectionView.props.Document.chromeStatus = 'collapsed'; + } else { + this.props.CollectionView.props.Document.chromeStatus = 'view-mode'; + } + } + render() { let headings = Array.from(this.Sections.keys()); let editableViewProps = { @@ -289,11 +301,18 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { {this.props.Document.sectionFilter ? Array.from(this.Sections.entries()).sort(this.sortFunc). map(section => this.section(section[0], section[1])) : this.section(undefined, this.filteredChildren)} - {(this.props.Document.sectionFilter && this.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? + {(this.props.Document.sectionFilter && this.props.CollectionView.props.Document.chromeStatus !== 'view-mode') ? <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" - style={{ width: (this.columnWidth / (headings.length + (this.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? 1 : 0))) - 10, marginTop: 10 }}> + style={{ width: (this.columnWidth / (headings.length + (this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' ? 1 : 0))) - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div> : null} + {this.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? <Switch + onChange={this.onToggle} + onClick={this.onToggle} + defaultChecked={this.props.CollectionView.props.Document.chromeStatus !== 'view-mode'} + checkedChildren="edit" + unCheckedChildren="view" + /> : null} </div> ); } diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index d8bed7e88..460359908 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -14,11 +14,17 @@ import { DocumentManager } from "../../util/DocumentManager"; import { SelectionManager } from "../../util/SelectionManager"; import "./CollectionStackingView.scss"; import { Docs } from "../../documents/Documents"; -import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { SchemaHeaderField, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ScriptField } from "../../../new_fields/ScriptField"; import { CompileScript } from "../../util/Scripting"; import { RichTextField } from "../../../new_fields/RichTextField"; +import { Transform } from "../../util/Transform"; +import { Flyout, anchorPoints } from "../DocumentDecorations"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faPalette } from '@fortawesome/free-solid-svg-icons'; + +library.add(faPalette); interface CSVFieldColumnProps { @@ -30,6 +36,7 @@ interface CSVFieldColumnProps { parent: CollectionStackingView; type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined; createDropTarget: (ele: HTMLDivElement) => void; + screenToLocalTransform: () => Transform; } @observer @@ -39,8 +46,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC private _dropRef: HTMLDivElement | null = null; private dropDisposer?: DragManager.DragDropDisposer; private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; + private _sensitivity: number = 16; @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; + @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; createColumnDropRef = (ele: HTMLDivElement | null) => { this._dropRef = ele; @@ -150,6 +160,14 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } @action + changeColumnColor = (color: string) => { + if (this.props.headingObject) { + this.props.headingObject.setColor(color); + this._color = color; + } + } + + @action pointerEntered = () => { if (SelectionManager.GetIsDragging()) { this._background = "#b4b4b4"; @@ -159,6 +177,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action pointerLeave = () => { this._background = "inherit"; + document.removeEventListener("pointermove", this.startDrag); } @action @@ -180,22 +199,25 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } startDrag = (e: PointerEvent) => { - let alias = Doc.MakeAlias(this.props.parent.props.Document); - let key = StrCast(this.props.parent.props.Document.sectionFilter); - let value = this.getValue(this._heading); - value = typeof value === "string" ? `"${value}"` : value; - let script = `return doc.${key} === ${value}`; - let compiled = CompileScript(script, { params: { doc: Doc.name } }); - if (compiled.compiled) { - let scriptField = new ScriptField(compiled); - alias.viewSpecScript = scriptField; - let dragData = new DragManager.DocumentDragData([alias], [alias.proto]); - DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY); - } + let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y); + if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { + let alias = Doc.MakeAlias(this.props.parent.props.Document); + let key = StrCast(this.props.parent.props.Document.sectionFilter); + let value = this.getValue(this._heading); + value = typeof value === "string" ? `"${value}"` : value; + let script = `return doc.${key} === ${value}`; + let compiled = CompileScript(script, { params: { doc: Doc.name } }); + if (compiled.compiled) { + let scriptField = new ScriptField(compiled); + alias.viewSpecScript = scriptField; + let dragData = new DragManager.DocumentDragData([alias], [alias.proto]); + DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY); + } - e.stopPropagation(); - document.removeEventListener("pointermove", this.startDrag); - document.removeEventListener("pointerup", this.pointerUp); + e.stopPropagation(); + document.removeEventListener("pointermove", this.startDrag); + document.removeEventListener("pointerup", this.pointerUp); + } } pointerUp = (e: PointerEvent) => { @@ -210,12 +232,45 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC e.stopPropagation(); e.preventDefault(); + let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY); + this._startDragPosition = { x: dx, y: dy }; + document.removeEventListener("pointermove", this.startDrag); document.addEventListener("pointermove", this.startDrag); document.removeEventListener("pointerup", this.pointerUp); document.addEventListener("pointerup", this.pointerUp); } + renderColorPicker = () => { + let selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + + let pink = PastelSchemaPalette.get("pink2"); + let purple = PastelSchemaPalette.get("purple4"); + let blue = PastelSchemaPalette.get("bluegreen1"); + let yellow = PastelSchemaPalette.get("yellow4"); + let red = PastelSchemaPalette.get("red2"); + let green = PastelSchemaPalette.get("bluegreen7"); + let cyan = PastelSchemaPalette.get("bluegreen5"); + let orange = PastelSchemaPalette.get("orange1"); + let gray = "#f1efeb"; + + return ( + <div className="collectionStackingView-colorPicker"> + <div className="colorOptions"> + <div className={"colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div> + <div className={"colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div> + <div className={"colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div> + <div className={"colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div> + <div className={"colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div> + <div className={"colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div> + <div className={"colorPicker" + (selected === green ? " active" : "")} style={{ backgroundColor: green }} onClick={() => this.changeColumnColor(green!)}></div> + <div className={"colorPicker" + (selected === cyan ? " active" : "")} style={{ backgroundColor: cyan }} onClick={() => this.changeColumnColor(cyan!)}></div> + <div className={"colorPicker" + (selected === orange ? " active" : "")} style={{ backgroundColor: orange }} onClick={() => this.changeColumnColor(orange!)}></div> + </div> + </div> + ); + } + render() { let cols = this.props.cols(); let key = StrCast(this.props.parent.props.Document.sectionFilter); @@ -242,7 +297,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC style={{ width: (style.columnWidth) / ((uniqueHeadings.length + - (this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? 1 : 0)) || 1) + (this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' ? 1 : 0)) || 1) }}> {/* the default bucket (no key value) has a tooltip that describes what it is. Further, it does not have a color and cannot be deleted. */} @@ -251,11 +306,19 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""} style={{ width: "100%", - background: this.props.headingObject && evContents !== `NO ${key.toUpperCase()} VALUE` ? - this.props.headingObject.color : "lightgrey", + background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey", color: "grey" }}> <EditableView {...headerEditableViewProps} /> + {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : + <div className="collectionStackingView-sectionColor"> + <Flyout anchorPoint={anchorPoints.TOP_CENTER} content={this.renderColorPicker()}> + <button className="collectionStackingView-sectionColorButton"> + <FontAwesomeIcon icon="palette" size="sm" /> + </button> + </ Flyout > + </div> + } {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> @@ -265,7 +328,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth}px `; return ( - <div key={heading} style={{ width: `${100 / ((uniqueHeadings.length + (this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? 1 : 0)) || 1)}%`, background: this._background }} + <div key={heading} style={{ width: `${100 / ((uniqueHeadings.length + (this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' ? 1 : 0)) || 1)}%`, background: this._background }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> {headingView} <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} @@ -283,9 +346,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {this.children(this.props.docList)} {singleColumn ? (null) : this.props.parent.columnDragger} </div> - {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? + {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode') ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / (uniqueHeadings.length + (this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? 1 : 0)) }}> + style={{ width: style.columnWidth / (uniqueHeadings.length + (this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' ? 1 : 0)) }}> <EditableView {...newEditableViewProps} /> </div> : null} </div> diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a15ed8f94..077f3f941 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,13 +1,15 @@ import { action, computed } from "mobx"; import * as rp from 'request-promise'; import CursorField from "../../../new_fields/CursorField"; -import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; +import { ScriptField } from "../../../new_fields/ScriptField"; import { BoolCast, Cast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { RouteStore } from "../../../server/RouteStore"; +import { Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs, DocumentOptions, DocumentType } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; @@ -19,10 +21,6 @@ import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; import React = require("react"); -import { MainView } from "../MainView"; -import { Utils } from "../../../Utils"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { CompileScript } from "../../util/Scripting"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -66,6 +64,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (res.success) { return res.result; } + else { + console.log(res.error); + } }); } return docs; @@ -112,6 +113,13 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @action protected drop(e: Event, de: DragManager.DropEvent): boolean { if (de.data instanceof DragManager.DocumentDragData) { + if (de.mods === "AltKey" && de.data.draggedDocuments.length) { + this.childDocs.map(doc => + Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, undefined) + ); + e.stopPropagation(); + return true; + } let added = false; if (de.data.dropAction || de.data.userDropAction) { added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index db3652ff6..990979109 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -7,6 +7,9 @@ border-radius: inherit; box-sizing: border-box; height: 100%; + width:100%; + position: absolute; + top:0; padding-top: 20px; padding-left: 10px; padding-right: 0px; diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss index 9d2c23d3e..509851ebb 100644 --- a/src/client/views/collections/CollectionVideoView.scss +++ b/src/client/views/collections/CollectionVideoView.scss @@ -6,6 +6,7 @@ top: 0; left:0; z-index: -1; + display:inline-table; } .collectionVideoView-time{ color : white; @@ -15,6 +16,14 @@ background-color: rgba(50, 50, 50, 0.2); transform-origin: left top; } +.collectionVideoView-snapshot{ + color : white; + top :25px; + right : 25px; + position: absolute; + background-color: rgba(50, 50, 50, 0.2); + transform-origin: left top; +} .collectionVideoView-play { width: 25px; height: 20px; diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index a264cc402..5185d9d0e 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -9,6 +9,7 @@ import "./CollectionVideoView.scss"; import React = require("react"); import { InkingControl } from "../InkingControl"; import { InkTool } from "../../../new_fields/InkField"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @observer @@ -21,18 +22,20 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { private get uIButtons() { let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); let curTime = NumCast(this.props.Document.curPage); - return ([<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> + return ([<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}> <span>{"" + Math.round(curTime)}</span> <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> </div>, + <div className="collectionVideoView-snapshot" key="time" onPointerDown={this.onSnapshot} style={{ transform: `scale(${scaling})` }}> + <FontAwesomeIcon icon="camera" size="lg" /> + </div>, VideoBox._showControls ? (null) : [ - <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> - {this._videoBox && this._videoBox.Playing ? "\"" : ">"} + <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling})` }}> + <FontAwesomeIcon icon={this._videoBox && this._videoBox.Playing ? "pause" : "play"} size="lg" /> </div>, - <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> + <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}> F - </div> - + </div> ]]); } @@ -56,6 +59,15 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { } } + @action + onSnapshot = (e: React.PointerEvent) => { + if (this._videoBox) { + this._videoBox.Snapshot(); + e.stopPropagation(); + e.preventDefault(); + } + } + _isclick = 0; @action onResetDown = (e: React.PointerEvent) => { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index b7ac8768f..34adc5840 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,11 +1,13 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faProjectDiagram, faSignature, faColumns, faSquare, faTh, faImage, faThList, faTree, faEllipsisV, faFingerprint, faLaptopCode } from '@fortawesome/free-solid-svg-icons'; +import { faEye } from '@fortawesome/free-regular-svg-icons'; +import { faColumns, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from "mobx-react"; import * as React from 'react'; -import { Doc, DocListCast, WidthSym, HeightSym } from '../../../new_fields/Doc'; +import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; +import { StrCast } from '../../../new_fields/Types'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; @@ -15,11 +17,7 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; import { CollectionTreeView } from "./CollectionTreeView"; -import { StrCast, PromiseValue } from '../../../new_fields/Types'; -import { DocumentType } from '../../documents/Documents'; -import { CollectionStackingViewChrome, CollectionViewBaseChrome } from './CollectionViewChromes'; -import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx'; -import { faEye } from '@fortawesome/free-regular-svg-icons'; +import { CollectionViewBaseChrome } from './CollectionViewChromes'; export const COLLECTION_BORDER_WIDTH = 2; library.add(faTh); @@ -45,6 +43,7 @@ export class CollectionView extends React.Component<FieldViewProps> { this._reactionDisposer = reaction(() => StrCast(this.props.Document.chromeStatus), () => { // chrome status is one of disabled, collapsed, or visible. this determines initial state from document + // chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar. let chromeStatus = this.props.Document.chromeStatus; if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) { runInAction(() => this._collapsed = true); @@ -85,7 +84,7 @@ export class CollectionView extends React.Component<FieldViewProps> { } else { return [ - (<CollectionViewBaseChrome CollectionView={this} type={type} collapse={this.collapse} />), + (<CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />), this.SubViewHelper(type, renderProps) ]; } diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 989315194..793cb7a8b 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -7,8 +7,6 @@ z-index: 9001; transition: top .5s; background: lightgrey; - transition: margin-top .5s; - background: lightgray; padding: 10px; .collectionViewChrome { @@ -16,6 +14,7 @@ grid-template-columns: 1fr auto; padding-bottom: 10px; border-bottom: .5px solid rgb(180, 180, 180); + overflow: hidden; .collectionViewBaseChrome { display: flex; @@ -37,9 +36,11 @@ } .collectionViewBaseChrome-collapse { - transition: all .5s; + transition: all .5s, opacity 0.3s; position: absolute; width: 40px; + transform-origin: top left; + // margin-top: 10px; } .collectionViewBaseChrome-viewSpecs { @@ -168,4 +169,55 @@ cursor: text; } } +} + +.collectionSchemaViewChrome-cont { + display: flex; + font-size: 10.5px; + + .collectionSchemaViewChrome-toggle { + display: flex; + margin-left: 10px; + } + + .collectionSchemaViewChrome-label { + text-transform: uppercase; + letter-spacing: 2px; + margin-right: 5px; + display: flex; + flex-direction: column; + justify-content: center; + } + + .collectionSchemaViewChrome-toggler { + width: 100px; + height: 41px; + background-color: black; + position: relative; + } + + .collectionSchemaViewChrome-togglerButton { + width: 47px; + height: 35px; + background-color: $light-color-secondary; + // position: absolute; + transition: all 0.5s ease; + // top: 3px; + margin-top: 3px; + color: gray; + letter-spacing: 2px; + text-transform: uppercase; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; + + &.on { + margin-left: 3px; + } + + &.off { + margin-left: 50px; + } + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 2bffe3cc0..1b2561953 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -17,6 +17,10 @@ import { CompileScript } from "../../util/Scripting"; import { ScriptField } from "../../../new_fields/ScriptField"; import { CollectionSchemaView } from "./CollectionSchemaView"; import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss"; +import { listSpec } from "../../../new_fields/Schema"; +import { List } from "../../../new_fields/List"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { threadId } from "worker_threads"; const datepicker = require('js-datepicker'); interface CollectionViewChromeProps { @@ -142,7 +146,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro `return ${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} ${keyRestrictionScript}` : `return ${keyRestrictionScript} ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` : "return true"; - let compiled = CompileScript(fullScript, { params: { doc: Doc.name } }); + let compiled = CompileScript(fullScript, { params: { doc: Doc.name }, typecheck: false }); if (compiled.compiled) { this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled); } @@ -185,11 +189,16 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro render() { return ( - <div className="collectionViewChrome-cont" style={{ marginTop: this._collapsed ? -70 : 0, height: 70 }}> + <div className="collectionViewChrome-cont" style={{ top: this._collapsed ? -70 : 0 }}> <div className="collectionViewChrome"> <div className="collectionViewBaseChrome"> <button className="collectionViewBaseChrome-collapse" - style={{ marginTop: this._collapsed ? 60 : 0, transform: `rotate(${this._collapsed ? 180 : 0}deg)` }} + style={{ + top: this._collapsed ? 70 : 10, + transform: `rotate(${this._collapsed ? 180 : 0}deg) scale(${this._collapsed ? 0.5 : 1}) translate(${this._collapsed ? "-100%, -100%" : "0, 0"})`, + opacity: (this._collapsed && !this.props.CollectionView.props.isSelected()) ? 0 : 0.9, + left: (this._collapsed ? 0 : "unset"), + }} title="Collapse collection chrome" onClick={this.toggleCollapse}> <FontAwesomeIcon icon="caret-up" size="2x" /> </button> @@ -204,10 +213,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option> </select> - <div className="collectionViewBaseChrome-viewSpecs"> + <div className="collectionViewBaseChrome-viewSpecs" style={{ display: this._collapsed ? "none" : "grid" }}> <input className="collectionViewBaseChrome-viewSpecsInput" placeholder="FILTER DOCUMENTS" value={this.filterValue ? this.filterValue.script.originalScript : ""} + onChange={(e) => { }} onPointerDown={this.openViewSpecs} /> <div className="collectionViewBaseChrome-viewSpecsMenu" onPointerDown={this.openViewSpecs} @@ -365,7 +375,9 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView @observer export class CollectionSchemaViewChrome extends React.Component<CollectionViewChromeProps> { + // private _textwrapAllRows: boolean = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0; + @undoBatch togglePreview = () => { let dividerWidth = 4; let borderWidth = Number(COLLECTION_BORDER_WIDTH); @@ -373,16 +385,56 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); let tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth; this.props.CollectionView.props.Document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0; + } + @undoBatch + @action + toggleTextwrap = async () => { + let textwrappedRows = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []); + if (textwrappedRows.length) { + this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>([]); + } else { + let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike) + = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]); + if (typeof docs === "function") { + docs = docs(); + } + docs = await docs; + if (docs instanceof Doc) { + let allRows = [docs[Id]]; + this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows); + } else { + let allRows = docs.map(doc => doc[Id]); + this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows); + } + } } render() { let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); + let textWrapped = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0; + return ( - <div className="collectionStackingViewChrome-cont"> - <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={previewWidth !== 0} onChange={this.togglePreview} />Show Preview</div> - </div> + <div className="collectionSchemaViewChrome-cont"> + <div className="collectionSchemaViewChrome-toggle"> + <div className="collectionSchemaViewChrome-label">Wrap Text: </div> + <div className="collectionSchemaViewChrome-toggler" onClick={this.toggleTextwrap}> + <div className={"collectionSchemaViewChrome-togglerButton" + (textWrapped ? " on" : " off")}> + {textWrapped ? "on" : "off"} + </div> + </div> + </div> + + <div className="collectionSchemaViewChrome-toggle"> + <div className="collectionSchemaViewChrome-label">Show Preview: </div> + <div className="collectionSchemaViewChrome-toggler" onClick={this.togglePreview}> + <div className={"collectionSchemaViewChrome-togglerButton" + (previewWidth !== 0 ? " on" : " off")}> + {previewWidth !== 0 ? "on" : "off"} + </div> + </div> + </div> + </div > ); } }
\ No newline at end of file diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx index 9baa250a6..1b59547d8 100644 --- a/src/client/views/collections/KeyRestrictionRow.tsx +++ b/src/client/views/collections/KeyRestrictionRow.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField"; +import { Doc } from "../../../new_fields/Doc"; interface IKeyRestrictionProps { contains: boolean; @@ -23,7 +24,10 @@ export default class KeyRestrictionRow extends React.Component<IKeyRestrictionPr parsedValue = parsed; type = "number"; } - let scriptText = `${this._contains ? "" : "!"}((doc.${this._key} as ${type})${type === "string" ? ".includes" : "<="}(${parsedValue}))`; + let scriptText = `${this._contains ? "" : "!"}(((doc.${this._key} && (doc.${this._key} as ${type})${type === "string" ? ".includes" : "<="}(${parsedValue}))) || + ((doc.data_ext && doc.data_ext.${this._key}) && (doc.data_ext.${this._key} as ${type})${type === "string" ? ".includes" : "<="}(${parsedValue}))))`; + // let doc = new Doc(); + // ((doc.data_ext && doc.data_ext!.text) && (doc.data_ext!.text as string).includes("hello")); this.props.script(scriptText); } else { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 7856f3718..59c77f1c9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,46 +1,45 @@ -import { action, computed, trace } from "mobx"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { faEye } from "@fortawesome/free-regular-svg-icons"; +import { faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; +import { action, computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast } from "../../../../new_fields/Doc"; +import { Doc, DocListCastAsync, HeightSym, WidthSym } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; +import { ScriptField } from "../../../../new_fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types"; -import { emptyFunction, returnOne } from "../../../../Utils"; +import { emptyFunction, returnOne, Utils } from "../../../../Utils"; +import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; +import { DocServer } from "../../../DocServer"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; +import { CompileScript } from "../../../util/Scripting"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; -import { SubmenuProps, ContextMenuProps } from "../../ContextMenuItem"; +import { ContextMenu } from "../../ContextMenu"; +import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingCanvas } from "../../InkingCanvas"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentContentsView } from "../../nodes/DocumentContentsView"; import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView"; import { pageSchema } from "../../nodes/ImageBox"; +import { OverlayElementOptions, OverlayView } from "../../OverlayView"; import PDFMenu from "../../pdf/PDFMenu"; import { CollectionSubView, SubCollectionViewProps } from "../CollectionSubView"; +import { ScriptBox } from "../../ScriptBox"; import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import v5 = require("uuid/v5"); -import { ScriptField } from "../../../../new_fields/ScriptField"; -import { OverlayView, OverlayElementOptions } from "../../OverlayView"; -import { ScriptBox } from "../../ScriptBox"; -import { CompileScript } from "../../../util/Scripting"; -import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faEye } from "@fortawesome/free-regular-svg-icons"; -import { faTable, faPaintBrush, faAsterisk, faExpandArrowsAlt, faCompressArrowsAlt, faCompass } from "@fortawesome/free-solid-svg-icons"; -import { undo } from "prosemirror-history"; -import { number } from "prop-types"; -import { ContextMenu } from "../../ContextMenu"; import DictationManager from "../../../util/DictationManager"; -library.add(faEye, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass); +library.add(faEye, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload); export const panZoomSchema = createSchema({ panX: "number", @@ -80,11 +79,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @computed get contentBounds() { let bounds = this.fitToBox && !this.isAnnotationOverlay ? this.ComputeContentBounds(this.elements.filter(e => e.bounds).map(e => e.bounds!)) : undefined; - return { + let res = { panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0, panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0, scale: (bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1) / this.parentScaling }; + if (res.scale === 0) res.scale = 1; + return res; } @computed get fitToBox() { return this.props.fitToBox || this.props.Document.fitToBox; } @@ -316,7 +317,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { onDragOver = (): void => { } - bringToFront = (doc: Doc) => { + bringToFront = (doc: Doc, sendToBack?: boolean) => { + if (sendToBack) { + doc.zIndex = 0; + return; + } const docs = this.childDocs; docs.slice().sort((doc1, doc2) => { if (doc1 === doc) return 1; @@ -494,14 +499,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } let docviews = docs.filter(doc => doc instanceof Doc).reduce((prev, doc) => { var page = NumCast(doc.page, -1); - let bounds: { x?: number, y?: number, width?: number, height?: number }; if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) { let minim = BoolCast(doc.isMinimized); if (minim === undefined || !minim) { - const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : {}; + const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : + { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") }; state = pos.state === undefined ? state : pos.state; prev.push({ - ele: <CollectionFreeFormDocumentView key={doc[Id]} x={pos.x} y={pos.y} width={pos.width} height={pos.height} {...this.getChildDocumentViewProps(doc)} />, + ele: <CollectionFreeFormDocumentView key={doc[Id]} + x={script ? pos.x : undefined} y={script ? pos.y : undefined} + width={script ? pos.width : undefined} height={script ? pos.height : undefined} {...this.getChildDocumentViewProps(doc)} />, bounds: (pos.x !== undefined && pos.y !== undefined && pos.width !== undefined && pos.height !== undefined) ? { x: pos.x, y: pos.y, width: pos.width, height: pos.height } : undefined }); } @@ -559,7 +566,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { CognitiveServices.Inking.Manager.analyzer(this.fieldExtensionDoc, relevantKeys, data.inkData); } - onContextMenu = () => { + onContextMenu = (e: React.MouseEvent) => { let layoutItems: ContextMenuProps[] = []; layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, @@ -581,6 +588,35 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { event: this.analyzeStrokes, icon: "paint-brush" }); + ContextMenu.Instance.addItem({ + description: "Import document", icon: "upload", event: () => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".zip"; + input.onchange = async _e => { + const files = input.files; + if (!files) return; + const file = files[0]; + let formData = new FormData(); + formData.append('file', file); + formData.append('remap', "true"); + const upload = Utils.prepend("/uploadDoc"); + const response = await fetch(upload, { method: "POST", body: formData }); + const json = await response.json(); + if (json === "error") { + return; + } + const doc = await DocServer.GetRefField(json); + if (!doc || !(doc instanceof Doc)) { + return; + } + const [x, y] = this.props.ScreenToLocalTransform().transformPoint(e.pageX, e.pageY); + doc.x = x, doc.y = y; + this.addDocument(doc, false); + }; + input.click(); + } + }); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 67bed284f..b9ee588dd 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -135,7 +135,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> doc.width = 200; docList.push(doc); } - let newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); + let newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); this.props.addDocument(newCol, false); } @@ -370,15 +370,25 @@ export class MarqueeView extends React.Component<MarqueeViewProps> let selRect = this.Bounds; let selection: Doc[] = []; this.props.activeDocuments().filter(doc => !doc.isBackground).map(doc => { - var z = NumCast(doc.zoomBasis, 1); var x = NumCast(doc.x); var y = NumCast(doc.y); - var w = NumCast(doc.width) / z; - var h = NumCast(doc.height) / z; + var w = NumCast(doc.width); + var h = NumCast(doc.height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { selection.push(doc); } }); + if (!selection.length) { + this.props.activeDocuments().map(doc => { + var x = NumCast(doc.x); + var y = NumCast(doc.y); + var w = NumCast(doc.width); + var h = NumCast(doc.height); + if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { + selection.push(doc); + } + }); + } return selection; } diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 91d4fb524..396233551 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -18,6 +18,7 @@ import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; import { FieldView } from "./FieldView"; import { WebBox } from "./WebBox"; +import { YoutubeBox } from "./../../apis/youtube/YoutubeBox"; import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import React = require("react"); import { FieldViewProps } from "./FieldView"; @@ -98,7 +99,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { if (this.props.renderDepth > 7) return (null); if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null); return <ObserverJsxParser - components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} + components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, YoutubeBox }} bindings={this.CreateBindings()} jsx={this.finalLayout} showWarnings={true} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index db8203167..39574db0f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -48,6 +48,7 @@ const JsxParser = require('react-jsx-parser').default; //TODO Why does this need library.add(fa.faTrash); library.add(fa.faShare); +library.add(fa.faDownload); library.add(fa.faExpandArrowsAlt); library.add(fa.faCompressArrowsAlt); library.add(fa.faLayerGroup); @@ -96,7 +97,7 @@ export interface DocumentViewProps { selectOnLoad: boolean; parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; - bringToFront: (doc: Doc) => void; + bringToFront: (doc: Doc, sendToBack?: boolean) => void; addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void; zoomToScale: (scale: number) => void; @@ -528,7 +529,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action makeBackground = (): void => { - this.props.Document.isBackground = true; + this.props.Document.isBackground = !this.props.Document.isBackground; + this.props.Document.isBackground && this.props.bringToFront(this.props.Document, true); } @undoBatch @@ -567,7 +569,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); cm.addItem({ description: "Transcribe Speech", event: this.listen, icon: "microphone" }); let makes: ContextMenuProps[] = []; - makes.push({ description: "Make Background", event: this.makeBackground, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); + makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Make Background", event: this.makeBackground, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" }); makes.push({ description: "Make Portal", event: () => { @@ -605,6 +607,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu copies.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); } + cm.addItem({ + description: "Download document", icon: "download", event: () => { + const a = document.createElement("a"); + const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); + a.href = url; + a.download = `DocExport-${this.props.Document[Id]}.zip`; + a.click(); + } + }); cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); type User = { email: string, userDocumentId: string }; let usersMenu: ContextMenuProps[] = []; @@ -698,7 +709,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} style={{ - pointerEvents: this.layoutDoc.isBackground ? "none" : "all", + pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all", color: foregroundColor, outlineColor: "maroon", outlineStyle: "dashed", diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index a24abb32e..247f7d1ea 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -43,7 +43,7 @@ .formattedTextBox-inner-rounded div, .formattedTextBox-inner div { - padding: 10px; + padding: 10px 10px; } .menuicon { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index fc0cc98aa..87b1d43c1 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,10 +1,11 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile, faTextHeight } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction, runInAction, computed, trace } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; +import { Node as ProsNode } from "prosemirror-model"; import { EditorState, Plugin, Transaction, Selection } from "prosemirror-state"; import { NodeType, Slice, Node, Fragment } from 'prosemirror-model'; import { EditorView } from "prosemirror-view"; @@ -33,6 +34,7 @@ import { Templates } from '../Templates'; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import React = require("react"); +import { For } from 'babel-types'; import { DateField } from '../../../new_fields/DateField'; import { Utils } from '../../../Utils'; import { MainOverlayTextBox } from '../MainOverlayTextBox'; @@ -49,6 +51,7 @@ export interface FormattedTextBoxProps { height?: string; color?: string; outer_div?: (domminus: HTMLElement) => void; + firstinstance?: boolean; } const richTextSchema = createSchema({ @@ -63,14 +66,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(FormattedTextBox, fieldStr); } + public static Instance: FormattedTextBox; private _ref: React.RefObject<HTMLDivElement>; private _outerdiv?: (dominus: HTMLElement) => void; private _proseRef?: HTMLDivElement; private _editorView: Opt<EditorView>; - private _toolTipTextMenu: TooltipTextMenu | undefined = undefined; + private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined; private _applyingChange: boolean = false; private _linkClicked = ""; private _reactionDisposer: Opt<IReactionDisposer>; + private _searchReactionDisposer?: Lambda; private _textReactionDisposer: Opt<IReactionDisposer>; private _proxyReactionDisposer: Opt<IReactionDisposer>; private dropDisposer?: DragManager.DragDropDisposer; @@ -100,6 +105,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return ""; } + public static getToolTip() { + return this._toolTipTextMenu; + } + @undoBatch public setFontColor(color: string) { let self = this; @@ -115,6 +124,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe constructor(props: FieldViewProps) { super(props); + //if (this.props.firstinstance) { + FormattedTextBox.Instance = this; + //} if (this.props.outer_div) { this._outerdiv = this.props.outer_div; } @@ -145,7 +157,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe // tx.setSelection(new Selection(tx.)) const state = this._editorView!.state; this._editorView!.dispatch(tx); - if (this._toolTipTextMenu) { + if (FormattedTextBox._toolTipTextMenu) { // this._toolTipTextMenu.makeLinkWithState(state) } e.stopPropagation(); @@ -161,7 +173,20 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this._editorView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); + if (state.selection.empty && FormattedTextBox._toolTipTextMenu) { + const marks = tx.storedMarks; + if (marks) { FormattedTextBox._toolTipTextMenu.mark_key_pressed(marks); } + } this._applyingChange = true; + const fieldkey = "preview"; + if (Object.keys(this.dataDoc).indexOf(fieldkey) !== -1) { + this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON())); + this.dataDoc[this.props.fieldKey + "_text"] = state.doc.textBetween(0, state.doc.content.size, "\n\n"); + } + else { + Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON())); + Doc.GetProto(this.dataDoc)[this.props.fieldKey + "_text"] = state.doc.textBetween(0, state.doc.content.size, "\n\n"); + } if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"); if (this.extensionDoc) this.extensionDoc.lastModified = new DateField(new Date(Date.now())); this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON())); @@ -175,6 +200,50 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + public highlightSearchTerms = (terms: String[]) => { + if (this._editorView && (this._editorView as any).docView) { + const fieldkey = "preview"; + const doc = this._editorView.state.doc; + const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); + doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => { + if (node.isLeaf && node.isText && node.text) { + let nodeText: String = node.text; + let tokens = nodeText.split(" "); + let start = pos; + tokens.forEach((word) => { + if (terms.includes(word) && this._editorView) { + this._editorView.dispatch(this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark)); + // else { + // this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark); + // } + } + start += word.length + 1; + }); + } + }); + } + } + + public unhighlightSearchTerms = () => { + if (this._editorView && (this._editorView as any).docView) { + const doc = this._editorView.state.doc; + const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); + doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => { + if (node.isLeaf && node.isText && node.text) { + if (node.marks.includes(mark) && this._editorView) { + this._editorView.dispatch(this._editorView.state.tr.removeMark(pos, pos + node.nodeSize, mark)); + } + } + }); + // const fieldkey = 'search_string'; + // if (Object.keys(this.props.Document).indexOf(fieldkey) !== -1) { + // this.props.Document[fieldkey] = undefined; + // } + // else this.props.Document.proto![fieldkey] = undefined; + // } + } + } + protected createDropTarget = (ele: HTMLDivElement) => { this._proseRef = ele; if (this.dropDisposer) { @@ -277,6 +346,22 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } }, { fireImmediately: true }); this.setupEditor(config, this.dataDoc, this.props.fieldKey); + + this._searchReactionDisposer = reaction(() => { + return StrCast(this.props.Document.search_string); + }, searchString => { + const fieldkey = 'preview'; + let preview = false; + // if (!this._editorView && Object.keys(this.props.Document).indexOf(fieldkey) !== -1) { + // preview = true; + // } + if (searchString) { + this.highlightSearchTerms([searchString]); + } + else { + this.unhighlightSearchTerms(); + } + }, { fireImmediately: true }); } clipboardTextSerializer = (slice: Slice): string => { @@ -411,7 +496,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe onPointerDown = (e: React.PointerEvent): void => { if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { e.stopPropagation(); - if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) { + if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) { //this._toolTipTextMenu.tooltip.style.opacity = "0"; } } @@ -467,7 +552,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } onPointerUp = (e: React.PointerEvent): void => { - if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) { + if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) { //this._toolTipTextMenu.tooltip.style.opacity = "1"; } if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { @@ -508,7 +593,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe tooltipTextMenuPlugin() { let myprops = this.props; - let self = this; + let self = FormattedTextBox; return new Plugin({ view(_editorView) { return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index dbe545048..b60ef41fd 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -93,17 +93,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD if (de.data instanceof DragManager.DocumentDragData) { de.data.droppedDocuments.forEach(action((drop: Doc) => { if (de.mods === "CtrlKey") { - let temp = Doc.MakeDelegate(drop); - this.props.Document.nativeWidth = Doc.GetProto(this.props.Document).nativeWidth = undefined; - this.props.Document.nativeHeight = Doc.GetProto(this.props.Document).nativeHeight = undefined; - this.props.Document.width = drop.width; - this.props.Document.height = drop.height; - Doc.GetProto(this.props.Document).type = DocumentType.TEMPLATE; - if (this.props.DataDoc && this.props.DataDoc.layout === this.props.Document) { - this.props.DataDoc.layout = temp; - } else { - this.props.Document.layout = temp; - } + Doc.ApplyTemplateTo(drop, this.props.Document, this.props.DataDoc); e.stopPropagation(); } else if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) { Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url); diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx index 0ea948c81..ecb3e9db4 100644 --- a/src/client/views/nodes/LinkEditor.tsx +++ b/src/client/views/nodes/LinkEditor.tsx @@ -290,7 +290,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> { let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); let index = keys.indexOf(""); if (index > -1) keys.splice(index, 1); - let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c)); + let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb")); let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); let ref = React.createRef<HTMLDivElement>(); diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx index 0cb216aa6..e04044266 100644 --- a/src/client/views/nodes/LinkMenuGroup.tsx +++ b/src/client/views/nodes/LinkMenuGroup.tsx @@ -72,7 +72,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); let index = keys.indexOf(""); if (index > -1) keys.splice(index, 1); - let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c)); + let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb")); let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); let ref = React.createRef<HTMLDivElement>(); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 34cb47b20..704030d85 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -8,7 +8,6 @@ import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; import { RouteStore } from "../../../server/RouteStore"; import { Utils } from "../../../Utils"; -import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; @@ -21,6 +20,10 @@ import { pageSchema } from "./ImageBox"; import "./VideoBox.scss"; import { library } from "@fortawesome/fontawesome-svg-core"; import { faVideo } from "@fortawesome/free-solid-svg-icons"; +import { CompileScript } from "../../util/Scripting"; +import { Doc } from "../../../new_fields/Doc"; +import { ScriptField } from "../../../new_fields/ScriptField"; +var path = require('path'); type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; const VideoDocument = makeInterface(positionSchema, pageSchema); @@ -87,6 +90,63 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD this._youtubePlayer && this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"); } + choosePath(url: string) { + if (url.indexOf(window.location.origin) === -1) { + return Utils.CorsProxy(url); + } + return url; + } + + @action public Snapshot() { + let width = NumCast(this.props.Document.width); + let height = NumCast(this.props.Document.height); + var canvas = document.createElement('canvas'); + canvas.width = 640; + canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); + var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions + if (ctx) { + ctx.rect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "blue"; + ctx.fill(); + this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); + } + + if (!this._videoRef) { // can't find a way to take snapshots of videos + let b = Docs.Create.ButtonDocument({ + x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), + width: 150, height: 50, title: NumCast(this.props.Document.curPage).toString() + }); + const script = CompileScript(`(self as any).curPage = ${NumCast(this.props.Document.curPage)}`, { + params: { this: Doc.name }, + capturedVariables: { self: this.props.Document }, + typecheck: false, + editable: true, + }); + if (script.compiled) { + b.onClick = new ScriptField(script); + this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(b, false); + } else { + console.log(script.errors.map(error => error.messageText).join("\n")); + } + } else { + //convert to desired file format + var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' + // if you want to preview the captured image, + let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); + VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { + if (returnedFilename) { + let url = this.choosePath(Utils.prepend(returnedFilename)); + let imageSummary = Docs.Create.ImageDocument(url, { + x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), + width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" + }); + this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false); + DocUtils.MakeLink(imageSummary, this.props.Document); + } + }); + } + } + @action updateTimecode = () => { this.player && (this.props.Document.curPage = this.player.currentTime); @@ -150,39 +210,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let subitems: ContextMenuProps[] = []; subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" }); subitems.push({ description: "Toggle Show Controls", event: action(() => VideoBox._showControls = !VideoBox._showControls), icon: "expand-arrows-alt" }); - let width = NumCast(this.props.Document.width); - let height = NumCast(this.props.Document.height); - subitems.push({ - description: "Take Snapshot", event: async () => { - var canvas = document.createElement('canvas'); - canvas.width = 640; - canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); - var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions - if (ctx) { - ctx.rect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = "blue"; - ctx.fill(); - this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); - } - - //convert to desired file format - var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' - // if you want to preview the captured image, - let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); - VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { - if (returnedFilename) { - let url = Utils.prepend(returnedFilename); - let imageSummary = Docs.Create.ImageDocument(url, { - x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), - width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" - }); - this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false); - DocUtils.MakeLink(imageSummary, this.props.Document); - } - }); - }, - icon: "expand-arrows-alt" - }); + subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Video Funcs...", subitems: subitems, icon: "video" }); } } diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index ccc3a72a9..11f3eb846 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -12,12 +12,8 @@ import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { Utils, returnFalse, emptyFunction, returnOne } from "../../../Utils"; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; import { SelectionManager } from "../../util/SelectionManager"; -import { indexOf } from "typescript-collections/dist/lib/arrays"; -import { map } from "bluebird"; import { ContextMenu } from "../ContextMenu"; -import { DocumentContentsView } from "../nodes/DocumentContentsView"; import { Transform } from "../../util/Transform"; -import { FieldView } from "../nodes/FieldView"; import { DocumentView } from "../nodes/DocumentView"; import { DocumentType } from "../../documents/Documents"; import React = require("react"); @@ -73,9 +69,6 @@ export default class PresentationElement extends React.Component<PresentationEle private backUpDoc: Doc | undefined; - - - constructor(props: PresentationElementProps) { super(props); this.selectedButtons = new Array(7); @@ -114,6 +107,9 @@ export default class PresentationElement extends React.Component<PresentationEle } } + /** + * Function that will be called to receive stored backUp for buttons + */ receiveButtonBackUp = async () => { //get the list that stores docs that keep track of buttons @@ -404,6 +400,10 @@ export default class PresentationElement extends React.Component<PresentationEle } + /** + * Function that opens up the option to open a element on right when navigated, + * instead of openening it as tab as default. + */ @action onRightTabClick = (e: React.MouseEvent) => { e.stopPropagation(); @@ -671,7 +671,6 @@ export default class PresentationElement extends React.Component<PresentationEle //This is used to add dragging as an event. onPointerEnter = (e: React.PointerEvent): void => { - // this.props.document.libraryBrush = true; if (e.buttons === 1 && SelectionManager.GetIsDragging()) { let selected = NumCast(this.props.mainDocument.selectedDoc, 0); @@ -688,7 +687,6 @@ export default class PresentationElement extends React.Component<PresentationEle //This is used to remove the dragging when dropped. onPointerLeave = (e: React.PointerEvent): void => { - // this.props.document.libraryBrush = false; //to get currently selected presentation doc let selected = NumCast(this.props.mainDocument.selectedDoc, 0); @@ -787,15 +785,23 @@ export default class PresentationElement extends React.Component<PresentationEle groupArray.push(tempStack.pop()!); } } - + /** + * This function is a getter to get if a document is in previewMode. + */ private get embedInline() { return BoolCast(this.props.document.embedOpen); } + /** + * This function sets document in presentation preview mode as the given value. + */ private set embedInline(value: boolean) { this.props.document.embedOpen = value; } + /** + * The function that recreates that context menu of presentation elements. + */ onContextMenu = (e: React.MouseEvent<HTMLDivElement>) => { e.preventDefault(); e.stopPropagation(); @@ -803,20 +809,19 @@ export default class PresentationElement extends React.Component<PresentationEle ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } + /** + * The function that is responsible for rendering the a preview or not for this + * presentation element. + */ renderEmbeddedInline = () => { if (!this.embedInline) { return (null); } - // return <ul key={this.props.document[Id] + "more"}> - // {TreeView.GetChildElements([this.props.document], "", new Doc(), undefined, "", (doc: Doc, relativeTo?: Doc, before?: boolean) => false, this.props.removeDocByRef, this.move, - // StrCast(this.props.document.dropAction) as dropActionType, (doc: Doc, dataDoc: Doc | undefined, where: string) => { }, Transform.Identity, () => ({ translateX: 0, translateY: 0 }), () => false, () => 400, 7)} - // </ul >; let propDocWidth = NumCast(this.props.document.nativeWidth); let propDocHeight = NumCast(this.props.document.nativeHeight); let scale = () => { let newScale = 175 / NumCast(this.props.document.nativeWidth, 175); - console.log("New Scale: ", newScale); return newScale; }; return ( @@ -836,7 +841,7 @@ export default class PresentationElement extends React.Component<PresentationEle addDocTab={returnFalse} renderDepth={1} PanelWidth={() => 350} - PanelHeight={() => 100} + PanelHeight={() => 90} focus={emptyFunction} selectOnLoad={false} parentActive={returnFalse} diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx index 2d63d41b5..e853c4070 100644 --- a/src/client/views/presentationview/PresentationList.tsx +++ b/src/client/views/presentationview/PresentationList.tsx @@ -7,8 +7,6 @@ import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; import { NumCast, StrCast } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/FieldSymbols"; import PresentationElement, { buttonIndex } from "./PresentationElement"; -import { DragManager } from "../../util/DragManager"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; import "../../../new_fields/Doc"; diff --git a/src/client/views/presentationview/PresentationModeMenu.tsx b/src/client/views/presentationview/PresentationModeMenu.tsx index b3edeb1e2..4de8da587 100644 --- a/src/client/views/presentationview/PresentationModeMenu.tsx +++ b/src/client/views/presentationview/PresentationModeMenu.tsx @@ -13,6 +13,10 @@ export interface PresModeMenuProps { closePresMode: () => void; } +/** + * This class is responsible for modeling of the Presentation Mode Menu. The menu allows + * user to navigate through presentation elements, and start/stop the presentation. + */ @observer export default class PresModeMenu extends React.Component<PresModeMenuProps> { @@ -21,18 +25,14 @@ export default class PresModeMenu extends React.Component<PresModeMenuProps> { @observable private _opacity: number = 1; @observable private _transition: string = "opacity 0.5s"; @observable private _transitionDelay: string = ""; - //@observable private Pinned: boolean = false; private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - @action - pointerEntered = (e: React.PointerEvent) => { - this._transition = "opacity 0.1s"; - this._transitionDelay = ""; - this._opacity = 1; - } - + /** + * The function that changes the coordinates of the menu, depending on the + * movement of the mouse when it's being dragged. + */ @action dragging = (e: PointerEvent) => { this._right -= e.movementX; @@ -42,6 +42,10 @@ export default class PresModeMenu extends React.Component<PresModeMenuProps> { e.preventDefault(); } + /** + * The function that removes the event listeners that are responsible for + * dragging of the menu. + */ dragEnd = (e: PointerEvent) => { document.removeEventListener("pointermove", this.dragging); document.removeEventListener("pointerup", this.dragEnd); @@ -49,20 +53,24 @@ export default class PresModeMenu extends React.Component<PresModeMenuProps> { e.preventDefault(); } + /** + * The function that starts the dragging of the presentation mode menu. When + * the lines on further right are clicked on. + */ dragStart = (e: React.PointerEvent) => { document.removeEventListener("pointermove", this.dragging); document.addEventListener("pointermove", this.dragging); document.removeEventListener("pointerup", this.dragEnd); document.addEventListener("pointerup", this.dragEnd); - let clientRect = this._mainCont.current!.getBoundingClientRect(); - - // runInAction(() => this._left = (clientRect.width - e.nativeEvent.offsetX) + clientRect.left); - // runInAction(() => this._top = e.nativeEvent.offsetY); e.stopPropagation(); e.preventDefault(); } + /** + * The function that is responsible for rendering the play or pause button, depending on the + * status of the presentation. + */ renderPlayPauseButton = () => { if (this.props.presStatus) { return <button title="Reset Presentation" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>; @@ -73,7 +81,7 @@ export default class PresModeMenu extends React.Component<PresModeMenuProps> { render() { return ( - <div className="presMenu-cont" onPointerEnter={this.pointerEntered} ref={this._mainCont} + <div className="presMenu-cont" ref={this._mainCont} style={{ right: this._right, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}> <button title="Back" className="presMenu-button" onClick={this.props.back}><FontAwesomeIcon icon={"arrow-left"} /></button> {this.renderPlayPauseButton()} diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index 97cbd4a24..65b09c833 100644 --- a/src/client/views/presentationview/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -6,6 +6,8 @@ right: 0; top: 0; bottom: 0; + letter-spacing: 2px; + } .presentationView-item { @@ -19,13 +21,11 @@ -ms-user-select: none; user-select: none; transition: all .1s; - //max-height: 250px; .documentView-node { - // height: auto !important; - // width: aut !important; + position: absolute; z-index: 1; } @@ -52,10 +52,11 @@ .presentationView-selected { background: gray; + color: black; } .presentationView-heading { - background: lightseagreen; + background: gray; padding: 10px; display: inline-block; width: 100%; @@ -67,6 +68,8 @@ font-size: 25px; display: inline-block; width: calc(100% - 200px); + letter-spacing: 2px; + } .presentation-icon { diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index ea85a8c6a..bea70f00b 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -16,7 +16,6 @@ import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, fa import { Docs } from "../../documents/Documents"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import PresentationViewList from "./PresentationList"; -import { ContextMenu } from "../ContextMenu"; import PresModeMenu from "./PresentationModeMenu"; import { CollectionDockingView } from "../collections/CollectionDockingView"; @@ -36,6 +35,7 @@ export interface PresViewProps { } const expandedWidth = 400; +const presMinWidth = 300; @observer export class PresentationView extends React.Component<PresViewProps> { @@ -375,7 +375,7 @@ export class PresentationView extends React.Component<PresViewProps> { //awaiting jump so that new scale can be found, since jumping is async await DocumentManager.Instance.jumpToDocument(curDoc, true); } else { - await DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); + await DocumentManager.Instance.jumpToDocument(curDoc, true, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); } let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); @@ -408,6 +408,10 @@ export class PresentationView extends React.Component<PresViewProps> { } + /** + * This function checks if right option is clicked on a presentation element, if not it does open it as a tab + * with help of CollectionDockingView. + */ jumpToTabOrRight = (curDocButtons: boolean[], curDoc: Doc) => { if (curDocButtons[buttonIndex.OpenRight]) { DocumentManager.Instance.jumpToDocument(curDoc, false); @@ -460,22 +464,6 @@ export class PresentationView extends React.Component<PresViewProps> { } } - //removing it from the backUp of selected Buttons - // let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); - // if (castedList) { - // castedList.forEach(async (doc, indexOfDoc) => { - // let curDoc = await doc; - // let curDocId = StrCast(curDoc.docId); - // if (curDocId === removedDoc[Id]) { - // if (castedList) { - // castedList.splice(indexOfDoc, 1); - // return; - // } - // } - // }); - - // } - //removing it from the backUp of selected Buttons let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); if (castedList) { @@ -513,13 +501,16 @@ export class PresentationView extends React.Component<PresViewProps> { } } + /** + * An alternative remove method that removes a doc from presentation by its actual + * reference. + */ public removeDocByRef = (doc: Doc) => { let indexOfDoc = this.childrenDocs.indexOf(doc); const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); if (value) { value.splice(indexOfDoc, 1)[0]; } - //this.RemoveDoc(indexOfDoc, true); if (indexOfDoc !== - 1) { return true; } @@ -618,6 +609,11 @@ export class PresentationView extends React.Component<PresViewProps> { this.curPresentation.presStatus = this.presStatus; } + /** + * This method is called to find the start document of presentation. So + * that when user presses on play, the correct presentation element will be + * selected. + */ findStartDocument = async () => { let docAtZero = await this.getDocAtIndex(0); if (docAtZero === undefined) { @@ -848,10 +844,11 @@ export class PresentationView extends React.Component<PresViewProps> { this.curPresentation.title = newTitle; } - addPressElem = (keyDoc: Doc, elem: PresentationElement) => { - this.presElementsMappings.set(keyDoc, elem); - } - + /** + * On pointer down element that is catched on resizer of te + * presentation view. Sets up the event listeners to change the size with + * mouse move. + */ _downsize = 0; onPointerDown = (e: React.PointerEvent) => { this._downsize = e.clientX; @@ -862,31 +859,41 @@ export class PresentationView extends React.Component<PresViewProps> { e.stopPropagation(); e.preventDefault(); } + /** + * Changes the size of the presentation view, with mouse move. + * Minimum size is set to 300, so that every button is visible. + */ @action onPointerMove = (e: PointerEvent) => { - this.curPresentation.width = Math.max(window.innerWidth - e.clientX, 300); + this.curPresentation.width = Math.max(window.innerWidth - e.clientX, presMinWidth); } + + /** + * The method that is called on pointer up event. It checks if the button is just + * clicked so that presentation view will be closed. The way it's done is to check + * for minimal pixel change like 4, and accept it as it's just a click on top of the dragger. + */ @action onPointerUp = (e: PointerEvent) => { if (Math.abs(e.clientX - this._downsize) < 4) { let presWidth = NumCast(this.curPresentation.width); - if (presWidth - 300 !== 0) { + if (presWidth - presMinWidth !== 0) { this.curPresentation.width = 0; } + if (presWidth === 0) { + this.curPresentation.width = presMinWidth; + } } document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); } - togglePresView = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - let width = NumCast(this.curPresentation.width); - if (width === 0) { - this.curPresentation.width = 300; - } - } + /** + * This function is a setter that opens up the + * presentation mode, by setting it's render flag + * to true. It also closes the presentation view. + */ @action openPresMode = () => { if (!this.presMode) { @@ -895,15 +902,23 @@ export class PresentationView extends React.Component<PresViewProps> { } } + /** + * This function closes the presentation mode by setting its + * render flag to false. It also opens up the presentation view. + * By setting it to it's minimum size. + */ @action closePresMode = () => { if (this.presMode) { this.presMode = false; - this.curPresentation.width = 300; + this.curPresentation.width = presMinWidth; } } + /** + * Function that is called to render the presentation mode, depending on its flag. + */ renderPresMode = () => { if (this.presMode) { return <PresModeMenu next={this.next} back={this.back} startOrResetPres={this.startOrResetPres} presStatus={this.presStatus} closePresMode={this.closePresMode} />; @@ -968,7 +983,7 @@ export class PresentationView extends React.Component<PresViewProps> { </div> <div className="mainView-libraryHandle" style={{ cursor: "ew-resize", right: `${width - 10}px`, backgroundColor: "white", opacity: this.opacity, transition: "0.7s opacity ease" }} - onPointerDown={this.onPointerDown} onClick={this.togglePresView}> + onPointerDown={this.onPointerDown}> <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} /> </div> {this.renderPresMode()} diff --git a/src/client/views/search/CheckBox.scss b/src/client/views/search/CheckBox.scss index af59d5fbf..cc858bec6 100644 --- a/src/client/views/search/CheckBox.scss +++ b/src/client/views/search/CheckBox.scss @@ -13,9 +13,9 @@ margin-top: 0px; .check-container:hover~.check-box { - background-color: $intermediate-color; + background-color: $darker-alt-accent; } - + .check-container { width: 40px; height: 40px; @@ -27,7 +27,8 @@ position: absolute; fill-opacity: 0; stroke-width: 4px; - stroke: white; + // stroke: white; + stroke: gray; } } @@ -55,5 +56,4 @@ margin-left: 15px; } -} - +}
\ No newline at end of file diff --git a/src/client/views/search/FieldFilters.scss b/src/client/views/search/FieldFilters.scss index ba0926140..e1d0d8df5 100644 --- a/src/client/views/search/FieldFilters.scss +++ b/src/client/views/search/FieldFilters.scss @@ -1,5 +1,12 @@ .field-filters { width: 100%; display: grid; - grid-template-columns: 18% 20% 60%; + // grid-template-columns: 18% 20% 60%; + grid-template-columns: 20% 25% 60%; +} + +.field-filters-required { + width: 100%; + display: grid; + grid-template-columns: 50% 50%; }
\ No newline at end of file diff --git a/src/client/views/search/FilterBox.scss b/src/client/views/search/FilterBox.scss index 1eb8963d7..ebb39460d 100644 --- a/src/client/views/search/FilterBox.scss +++ b/src/client/views/search/FilterBox.scss @@ -3,22 +3,25 @@ .filter-form { padding: 25px; - width: 600px; - background: $dark-color; + width: 440px; + background: whitesmoke; position: relative; right: 1px; - color: $light-color; + color: grey; flex-direction: column; display: inline-block; transform-origin: top; overflow: auto; + border-radius: 15px; + box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; + border: solid #BBBBBBBB 1px; .top-filter-header { #header { text-transform: uppercase; letter-spacing: 2px; - font-size: 25; + font-size: 13; width: 80%; } @@ -26,13 +29,13 @@ width: 20%; opacity: .6; position: relative; - display: inline-block; + display: block; .line { display: block; background: $alt-accent; - width: $width-line; - height: $height-line; + width: 20; + height: 3; position: absolute; right: 0; border-radius: ($height-line / 2); @@ -69,9 +72,10 @@ display: flex; align-items: center; margin-bottom: 10px; + letter-spacing: 2px; .filter-title { - font-size: 18; + font-size: 13; text-transform: uppercase; margin-top: 10px; margin-bottom: 10px; @@ -96,6 +100,7 @@ -moz-transition: all 0.2s ease-in-out; -o-transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out; + text-align: center; } } } @@ -105,4 +110,72 @@ border-top-style: solid; padding-top: 10px; } +} + +.active-filters { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + width: 100%; + margin-right: 30px; + position: relative; + + .active-icon { + max-width: 40px; + flex: initial; + + &.icon{ + width: 40px; + text-align: center; + margin-bottom: 5px; + position: absolute; + } + + &.container { + display: flex; + flex-direction: column; + width: 40px; + } + + &.description { + text-align: center; + top: 40px; + position: absolute; + width: 40px; + font-size: 9px; + opacity: 0; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + } + + &.icon:hover + .description { + opacity: 1; + } + } + + .col-icon { + height: 35px; + margin-left: 5px; + width: 35px; + background-color: black; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + + .save-filter, + .reset-filter, + .all-filter { + background-color: gray; + } + + .save-filter:hover, + .reset-filter:hover, + .all-filter:hover { + background-color: $darker-alt-accent; + } + } }
\ No newline at end of file diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index 706d1eb7f..995ddd5c3 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -2,25 +2,26 @@ import * as React from 'react'; import { observer } from 'mobx-react'; import { observable, action } from 'mobx'; import "./SearchBox.scss"; -import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { faTimes, faCheckCircle, faObjectGroup } from '@fortawesome/free-solid-svg-icons'; import { library } from '@fortawesome/fontawesome-svg-core'; import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { DocumentType } from '../../documents/Documents'; import { Cast, StrCast } from '../../../new_fields/Types'; import * as _ from "lodash"; -import { ToggleBar } from './ToggleBar'; import { IconBar } from './IconBar'; import { FieldFilters } from './FieldFilters'; import { SelectionManager } from '../../util/SelectionManager'; import { DocumentView } from '../nodes/DocumentView'; import { CollectionFilters } from './CollectionFilters'; -import { NaviconButton } from './NaviconButton'; import * as $ from 'jquery'; import "./FilterBox.scss"; import { SearchBox } from './SearchBox'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; library.add(faTimes); +library.add(faCheckCircle); +library.add(faObjectGroup); export enum Keys { TITLE = "title", @@ -35,11 +36,18 @@ export class FilterBox extends React.Component { public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.HIST, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB]; //if true, any keywords can be used. if false, all keywords are required. + //this also serves as an indicator if the word status filter is applied @observable private _basicWordStatus: boolean = true; @observable private _filterOpen: boolean = false; + //if icons = all icons, then no icon filter is applied @observable private _icons: string[] = this._allIcons; + //if all of these are true, no key filter is applied + @observable private _anyKeywordStatus: boolean = true; + @observable private _allKeywordStatus: boolean = true; @observable private _titleFieldStatus: boolean = true; @observable private _authorFieldStatus: boolean = true; + @observable private _dataFieldStatus: boolean = true; + //this also serves as an indicator if the collection status filter is applied @observable public _deletedDocsStatus: boolean = false; @observable private _collectionStatus = false; @observable private _collectionSelfStatus = true; @@ -114,10 +122,9 @@ export class FilterBox extends React.Component { @action.bound resetFilters = () => { - ToggleBar.Instance.resetToggle(); + this._basicWordStatus = true; IconBar.Instance.selectAll(); FieldFilters.Instance.resetFieldFilters(); - CollectionFilters.Instance.resetCollectionFilters(); } basicRequireWords(query: string): string { @@ -259,6 +266,40 @@ export class FilterBox extends React.Component { return finalDocs; } + getABCicon() { + return ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.8 87.8" height="35"> + <path d="M25.4 47.9c-1.3 1.3-1.9 2.8-1.9 4.8 0 3.8 2.3 6.1 6.1 6.1 5.1 0 8-3.3 9-6.2 0.2-0.7 0.4-1.4 0.4-2.1v-6.1c-0.1 0-0.1 0-0.2 0C32.2 44.5 27.7 45.6 25.4 47.9z" /> + <path d="M64.5 28.6c-2.2 0-4.1 1.5-4.7 3.8l0 0.2c-0.1 0.3-0.1 0.7-0.1 1.1v3.3c0 0.4 0.1 0.8 0.2 1.1 0.6 2.2 2.4 3.6 4.6 3.6 3.2 0 5.2-2.6 5.2-6.7C69.5 31.8 68 28.6 64.5 28.6z" /> + <path d="M43.9 0C19.7 0 0 19.7 0 43.9s19.7 43.9 43.9 43.9 43.9-19.6 43.9-43.9S68.1 0 43.9 0zM40.1 65.5l-0.5-4c-3 3.1-7.4 4.9-12.1 4.9 -6.8 0-13.6-4.4-13.6-12.8 0-4 1.3-7.4 4-10 4.1-4.1 11.1-6.2 20.8-6.3 0-5.5-2.9-8.4-8.3-8.4 -3.6 0-7.4 1.1-10.2 2.9l-1.1 0.7 -2.4-6.9 0.7-0.4c3.7-2.4 8.9-3.8 14.1-3.8 10.9 0 16.7 6.2 16.7 17.9V54.6c0 4.1 0.2 7.2 0.7 9.7L49 65.5H40.1zM65.5 67.5c1.8 0 3-0.5 4-0.9l0.5-0.2 0.8 3.4 -0.3 0.2c-1 0.5-3 1.1-5.5 1.1 -5.8 0-9.7-4-9.7-9.9 0-6.1 4.3-10.3 10.4-10.3 2.1 0 4 0.5 4.9 1l0.3 0.2 -1 3.5 -0.5-0.3c-0.7-0.4-1.8-0.8-3.7-0.8 -3.7 0-6.1 2.6-6.1 6.6C59.5 64.8 61.9 67.5 65.5 67.5zM65 45.3c-2.5 0-4.5-0.9-5.9-2.7l-0.1 2.3h-3.8l0-0.5c0.1-1.2 0.2-3.1 0.2-4.8V16.7h4.3v10.8c1.4-1.6 3.5-2.5 6-2.5 2.2 0 4.1 0.8 5.5 2.3 1.8 1.8 2.8 4.5 2.8 7.7C73.8 42.1 69.3 45.3 65 45.3z" /> + </svg> + ); + } + + getTypeIcon() { + return ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.8 87.8" height="35"> + <path d="M43.9 0C19.7 0 0 19.7 0 43.9s19.7 43.9 43.9 43.9 43.9-19.6 43.9-43.9S68.1 0 43.9 0zM43.9 12.2c4.1 0 7.5 3.4 7.5 7.5 0 4.1-3.4 7.5-7.5 7.5 -4.1 0-7.5-3.4-7.5-7.5C36.4 15.5 39.7 12.2 43.9 12.2zM11.9 50.4l7.5-13 7.5 13H11.9zM47.6 75.7h-7.5l-3.7-6.5 3.8-6.5h7.5l3.8 6.5L47.6 75.7zM70.7 70.7c-0.2 0.2-0.4 0.3-0.7 0.3s-0.5-0.1-0.7-0.3l-25.4-25.4 -25.4 25.4c-0.2 0.2-0.4 0.3-0.7 0.3s-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1 0-1.4l25.4-25.4 -25.4-25.4c-0.4-0.4-0.4-1 0-1.4s1-0.4 1.4 0l25.4 25.4 25.4-25.4c0.4-0.4 1-0.4 1.4 0s0.4 1 0 1.4l-25.4 25.4 25.4 25.4C71.1 69.7 71.1 70.3 70.7 70.7zM61.4 51.4v-15h15v15H61.4z" /> + </svg> + ); + } + + getKeyIcon() { + return ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.8 87.8" height="35"> + <path d="M38.5 32.4c0 3.4-2.7 6.1-6.1 6.1 -3.4 0-6.1-2.7-6.1-6.1 0-3.4 2.8-6.1 6.1-6.1C35.8 26.3 38.5 29 38.5 32.4zM87.8 43.9c0 24.2-19.6 43.9-43.9 43.9S0 68.1 0 43.9C0 19.7 19.7 0 43.9 0S87.8 19.7 87.8 43.9zM66.8 60.3L50.2 43.7c-0.5-0.5-0.6-1.2-0.4-1.8 2.4-5.6 1.1-12.1-3.2-16.5 -5.9-5.8-15.4-5.8-21.2 0l0 0c-4.3 4.3-5.6 10.8-3.2 16.5 3.2 7.6 12 11.2 19.7 8 0.6-0.3 1.4-0.1 1.8 0.4l3.1 3.1h3.9c1.2 0 2.2 1 2.2 2.2v3.6h3.6c1.2 0 2.2 1 2.2 2.2v4l1.6 1.6h6.5V60.3z" /> + </svg> + ); + } + + getColIcon() { + return ( + <div className="col-icon"> + <FontAwesomeIcon icon={faObjectGroup} size="lg" /> + </div> + ); + } + @action.bound openFilter = () => { this._filterOpen = !this._filterOpen; @@ -268,10 +309,9 @@ export class FilterBox extends React.Component { //if true, any keywords can be used. if false, all keywords are required. @action.bound - handleWordQueryChange = () => { this._basicWordStatus = !this._basicWordStatus; } - - @action.bound - getBasicWordStatus() { return this._basicWordStatus; } + handleWordQueryChange = () => { + this._basicWordStatus = !this._basicWordStatus; + } @action.bound updateIcon(newArray: string[]) { this._icons = newArray; } @@ -290,16 +330,10 @@ export class FilterBox extends React.Component { } @action.bound - toggleFieldOpen() { this._fieldOpen = !this._fieldOpen; } - - @action.bound - toggleColOpen() { this._colOpen = !this._colOpen; } - - @action.bound - toggleTypeOpen() { this._typeOpen = !this._typeOpen; } + updateAnyKeywordStatus(newStat: boolean) { this._anyKeywordStatus = newStat; } @action.bound - toggleWordStatusOpen() { this._wordStatusOpen = !this._wordStatusOpen; } + updateAllKeywordStatus(newStat: boolean) { this._allKeywordStatus = newStat; } @action.bound updateTitleStatus(newStat: boolean) { this._titleFieldStatus = newStat; } @@ -319,6 +353,8 @@ export class FilterBox extends React.Component { @action.bound updateParentCollectionStatus(newStat: boolean) { this._collectionParentStatus = newStat; } + getAnyKeywordStatus() { return this._anyKeywordStatus; } + getAllKeywordStatus() { return this._allKeywordStatus; } getCollectionStatus() { return this._collectionStatus; } getSelfCollectionStatus() { return this._collectionSelfStatus; } getParentCollectionStatus() { return this._collectionParentStatus; } @@ -326,6 +362,31 @@ export class FilterBox extends React.Component { getAuthorStatus() { return this._authorFieldStatus; } getDataStatus() { return this._deletedDocsStatus; } + getActiveFilters() { + console.log(this._authorFieldStatus, this._titleFieldStatus, this._dataFieldStatus); + return ( + <div className="active-filters"> + {!this._basicWordStatus ? <div className="active-icon container"> + <div className="active-icon icon">{this.getABCicon()}</div> + <div className="active-icon description">Required Words Applied</div> + </div> : undefined} + {!(this._icons.length === 9) ? <div className="active-icon container"> + <div className="active-icon icon">{this.getTypeIcon()}</div> + <div className="active-icon description">Type Filters Applied</div> + </div> : undefined} + {!(this._authorFieldStatus && this._dataFieldStatus && this._titleFieldStatus) ? + <div className="active-icon container"> + <div className="active-icon icon">{this.getKeyIcon()}</div> + <div className="active-icon description">Field Filters Applied</div> + </div> : undefined} + {this._collectionStatus ? <div className="active-icon container"> + <div className="active-icon icon">{this.getColIcon()}</div> + <div className="active-icon description">Collection Filters Active</div> + </div> : undefined} + </div> + ) + } + // Useful queries: // Delegates of a document: {!join from=id to=proto_i}id:{protoId} // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId} //id of collections prototype @@ -334,11 +395,13 @@ export class FilterBox extends React.Component { <div> <div style={{ display: "flex", flexDirection: "row-reverse" }}> <SearchBox /> + {this.getActiveFilters()} </div> {this._filterOpen ? ( <div className="filter-form" onPointerDown={this.stopProp} id="filter-form" style={this._filterOpen ? { display: "flex" } : { display: "none" }}> <div className="top-filter-header" style={{ display: "flex", width: "100%" }}> <div id="header">Filter Search Results</div> + <div style={{ marginLeft: "auto" }}></div> <div className="close-icon" onClick={this.closeFilter}> <span className="line line-1"></span> <span className="line line-2"></span></div> @@ -347,33 +410,20 @@ export class FilterBox extends React.Component { <div className="filter-div"> <div className="filter-header"> <div className='filter-title words'>Required words</div> - <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleWordStatusOpen} /></div> </div> <div className="filter-panel" > - <ToggleBar handleChange={this.handleWordQueryChange} getStatus={this.getBasicWordStatus} - originalStatus={this._basicWordStatus} optionOne={"Include Any Keywords"} optionTwo={"Include All Keywords"} /> + <button className="all-filter" onClick={this.handleWordQueryChange}>Include All Keywords</button> </div> </div> <div className="filter-div"> <div className="filter-header"> <div className="filter-title icon">Filter by type of node</div> - <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleTypeOpen} /></div> </div> <div className="filter-panel"><IconBar /></div> </div> <div className="filter-div"> <div className="filter-header"> - <div className='filter-title collection'>Search in current collections</div> - <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleColOpen} /></div> - </div> - <div className="filter-panel"><CollectionFilters - updateCollectionStatus={this.updateCollectionStatus} updateParentCollectionStatus={this.updateParentCollectionStatus} updateSelfCollectionStatus={this.updateSelfCollectionStatus} - collectionStatus={this._collectionStatus} collectionParentStatus={this._collectionParentStatus} collectionSelfStatus={this._collectionSelfStatus} /></div> - </div> - <div className="filter-div"> - <div className="filter-header"> <div className="filter-title field">Filter by Basic Keys</div> - <div style={{ marginLeft: "auto" }}><NaviconButton onClick={this.toggleFieldOpen} /></div> </div> <div className="filter-panel"><FieldFilters titleFieldStatus={this._titleFieldStatus} dataFieldStatus={this._deletedDocsStatus} authorFieldStatus={this._authorFieldStatus} @@ -381,13 +431,12 @@ export class FilterBox extends React.Component { </div> </div> <div className="filter-buttons" style={{ display: "flex", justifyContent: "space-around" }}> - <button className="minimize-filter" onClick={this.minimizeAll}>Minimize All</button> - <button className="advanced-filter" >Advanced Filters</button> <button className="save-filter" >Save Filters</button> <button className="reset-filter" onClick={this.resetFilters}>Reset Filters</button> </div> </div> - ) : undefined} + ) : + undefined} </div> ); } diff --git a/src/client/views/search/IconBar.scss b/src/client/views/search/IconBar.scss index e384722ce..2555ad271 100644 --- a/src/client/views/search/IconBar.scss +++ b/src/client/views/search/IconBar.scss @@ -4,9 +4,8 @@ display: flex; justify-content: space-evenly; align-items: center; - height: 40px; + height: 35px; width: 100%; flex-wrap: wrap; margin-bottom: 10px; -} - +}
\ No newline at end of file diff --git a/src/client/views/search/IconButton.scss b/src/client/views/search/IconButton.scss index 94b294ba5..d1853177e 100644 --- a/src/client/views/search/IconButton.scss +++ b/src/client/views/search/IconButton.scss @@ -4,13 +4,15 @@ display: flex; flex-direction: column; align-items: center; - width: 45px; + width: 30px; height: 60px; .type-icon { - height: 45px; - width: 45px; + height: 30px; + width: 30px; color: $light-color; + // background-color: rgb(194, 194, 197); + background-color: gray; border-radius: 50%; display: flex; justify-content: center; @@ -22,8 +24,8 @@ font-size: 2em; .fontawesome-icon { - height: 24px; - width: 24px; + height: 15px; + width: 15px } } @@ -44,7 +46,7 @@ transform: scale(1.1); background-color: $darker-alt-accent; opacity: 1; - + +.filter-description { opacity: 1; } diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx index bfe2c7d0b..5d23f6eeb 100644 --- a/src/client/views/search/IconButton.tsx +++ b/src/client/views/search/IconButton.tsx @@ -13,6 +13,7 @@ import { IconBar } from './IconBar'; import { props } from 'bluebird'; import { FilterBox } from './FilterBox'; import { Search } from '../../../server/Search'; +import { gravity } from 'sharp'; library.add(faSearch); library.add(faObjectGroup); @@ -123,11 +124,11 @@ export class IconButton extends React.Component<IconButtonProps>{ selected = { opacity: 1, - backgroundColor: "#c2c2c5" //$alt-accent + backgroundColor: "rgb(128, 128, 128)" }; notSelected = { - opacity: 0.6, + opacity: 0.2, }; hoverStyle = { diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 109b88ac9..fcdc79220 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -45,6 +45,11 @@ top: 300px; display: flex; flex-direction: column; + margin-right: 72px; + // height: 560px; + height: 100%; + // overflow: hidden; + // overflow-y: auto; max-height: 560px; overflow: hidden; overflow-y: auto; diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 562594210..0390359b3 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -21,11 +21,18 @@ import { DocumentView } from "../nodes/DocumentView"; import { SearchBox } from "./SearchBox"; import "./SearchItem.scss"; import "./SelectorContextMenu.scss"; +import { RichTextField } from "../../../new_fields/RichTextField"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { MarqueeView } from "../collections/collectionFreeForm/MarqueeView"; +import { SelectionManager } from "../../util/SelectionManager"; +import { ObjectField } from "../../../new_fields/ObjectField"; import { ContextMenu } from "../ContextMenu"; import { faFile } from '@fortawesome/free-solid-svg-icons'; +import { DocServer } from "../../DocServer"; export interface SearchItemProps { doc: Doc; + query?: string; highlighting: string[]; } @@ -86,7 +93,7 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> { SetupDrag(item, () => doc.col, undefined, undefined, undefined, undefined, () => SearchBox.Instance.closeSearch())}> <FontAwesomeIcon icon={faStickyNote} /> </div> - <a className="title" onClick={this.getOnClick(doc)}>{doc.col.title}</a> + <a onClick={this.getOnClick(doc)}>{doc.col.title}</a> </div>; })} </div> @@ -94,27 +101,115 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> { } } +export interface LinkMenuProps { + doc1: Doc; + doc2: Doc; +} + +@observer +export class LinkContextMenu extends React.Component<LinkMenuProps> { + + highlightDoc = (doc: Doc) => { + return () => { + doc.libraryBrush = true; + }; + } + + unHighlightDoc = (doc: Doc) => { + return () => { + doc.libraryBrush = false; + }; + } + + getOnClick(col: Doc) { + return () => { + CollectionDockingView.Instance.AddRightSplit(col, undefined); + }; + } + + render() { + return ( + <div className="parents"> + <p className="contexts">Anchors:</p> + <div className="collection"><a onMouseEnter={this.highlightDoc(this.props.doc1)} onMouseLeave={this.unHighlightDoc(this.props.doc1)} onClick={this.getOnClick(this.props.doc1)}>Doc 1: {this.props.doc2.title}</a></div> + <div><a onMouseEnter={this.highlightDoc(this.props.doc2)} onMouseLeave={this.unHighlightDoc(this.props.doc2)} onClick={this.getOnClick(this.props.doc2)}>Doc 2: {this.props.doc1.title}</a></div> + </div> + ) + } + +} + @observer export class SearchItem extends React.Component<SearchItemProps> { @observable _selected: boolean = false; + private _previewDoc?: Doc; onClick = () => { // I dont think this is the best functionality because clicking the name of the collection does that. Change it back if you'd like DocumentManager.Instance.jumpToDocument(this.props.doc, false); + if (this.props.doc.data instanceof RichTextField) { + this.highlightTextBox(this.props.doc); + } // CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined); } @observable _useIcons = true; @observable _displayDim = 50; - @computed - public get DocumentIcon() { + highlightTextBox = (doc: Doc) => { + if (this.props.query) { + const fieldkey = 'search_string'; + if (Object.keys(doc).indexOf(fieldkey) === -1) { + doc.search_string = this.props.query; + } + else { + doc.search_string = undefined; + } + + } + } + + fitToBox = () => { + let bounds = Doc.ComputeContentBounds([this.props.doc]); + return [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / Math.max((bounds.b - bounds.y), (bounds.r - bounds.x)), this._displayDim]; + } + + componentWillUnmount() { + if (this._previewDoc) { + DocServer.DeleteDocument(this._previewDoc[Id]); + } + } + + + //@computed + @action + public DocumentIcon() { + let layoutresult = StrCast(this.props.doc.type); if (!this._useIcons) { + let renderDoc = this.props.doc; + //let box: number[] = []; + if (layoutresult.indexOf(DocumentType.COL) !== -1) { + renderDoc = Doc.MakeDelegate(renderDoc); + let bounds = DocListCast(renderDoc.data).reduce((bounds, doc) => { + var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; + let [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()]; + return { + x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), + r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) + }; + }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); + let box = () => [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / (bounds.r - bounds.x), this._displayDim]; + } let returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE); let returnYDimension = () => this._displayDim; - let scale = () => returnXDimension() / NumCast(this.props.doc.nativeWidth, returnXDimension()); - return <div - onPointerDown={action(() => { this._useIcons = !this._useIcons; this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE); })} + let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension()); + let newRenderDoc = Doc.MakeDelegate(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt + this._previewDoc = newRenderDoc; + const docview = <div + onPointerDown={action(() => { + this._useIcons = !this._useIcons; + this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE); + })} onPointerEnter={action(() => this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE))} onPointerLeave={action(() => this._displayDim = 50)} > <DocumentView @@ -138,9 +233,15 @@ export class SearchItem extends React.Component<SearchItemProps> { ContentScaling={scale} /> </div>; + const data = renderDoc.data; + if (data instanceof ObjectField) newRenderDoc.data = ObjectField.MakeCopy(data); + newRenderDoc.preview = true; + newRenderDoc.search_string = this.props.query; + return docview; + } + if (this._previewDoc) { + DocServer.DeleteDocument(this._previewDoc[Id]); } - - let layoutresult = StrCast(this.props.doc.type); let button = layoutresult.indexOf(DocumentType.PDF) !== -1 ? faFilePdf : layoutresult.indexOf(DocumentType.IMG) !== -1 ? faImage : layoutresult.indexOf(DocumentType.TEXT) !== -1 ? faStickyNote : @@ -239,6 +340,8 @@ export class SearchItem extends React.Component<SearchItemProps> { } render() { + const doc1 = Cast(this.props.doc.anchor1, Doc); + const doc2 = Cast(this.props.doc.anchor2, Doc); return ( <div className="search-overview" onPointerDown={this.pointerDown} onContextMenu={this.onContextMenu}> <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result" @@ -262,7 +365,8 @@ export class SearchItem extends React.Component<SearchItemProps> { </div> </div> <div className="searchBox-instances"> - <SelectorContextMenu {...this.props} /> + {(doc1 instanceof Doc && doc2 instanceof Doc) ? this.props.doc.type === DocumentType.LINK ? <LinkContextMenu doc1={doc1} doc2={doc2} /> : + <SelectorContextMenu {...this.props} /> : null} </div> </div> ); diff --git a/src/client/views/search/SelectorContextMenu.scss b/src/client/views/search/SelectorContextMenu.scss index 49f77b9bf..48cacc608 100644 --- a/src/client/views/search/SelectorContextMenu.scss +++ b/src/client/views/search/SelectorContextMenu.scss @@ -3,6 +3,7 @@ .parents { background: $lighter-alt-accent; padding: 10px; + // width: 300px; .contexts { text-transform: uppercase; diff --git a/src/client/views/search/ToggleBar.scss b/src/client/views/search/ToggleBar.scss index 633a194fe..79f866acb 100644 --- a/src/client/views/search/ToggleBar.scss +++ b/src/client/views/search/ToggleBar.scss @@ -16,11 +16,15 @@ -moz-transition: all 0.2s ease-in-out; -o-transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out; + color: gray; + font-size: 13; } } .toggle-bar { - height: 50px; + // height: 50px; + height: 30px; + width: 100px; background-color: $alt-accent; border-radius: 10px; padding: 5px; @@ -28,7 +32,8 @@ align-items: center; .toggle-button { - width: 275px; + // width: 275px; + width: 40px; height: 100%; border-radius: 10px; background-color: $light-color; diff --git a/src/client/views/search/ToggleBar.tsx b/src/client/views/search/ToggleBar.tsx index 178578c5c..a30104089 100644 --- a/src/client/views/search/ToggleBar.tsx +++ b/src/client/views/search/ToggleBar.tsx @@ -59,6 +59,7 @@ export class ToggleBar extends React.Component<ToggleBarProps>{ this._forwardTimeline.play(); this._forwardTimeline.reverse(); this.props.handleChange(); + console.log(this.props.getStatus()) } @action.bound diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx index 2b3eed154..24db3f934 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -10,6 +10,7 @@ import { List } from '../new_fields/List'; import { URLField } from '../new_fields/URLField'; import { EditableView } from '../client/views/EditableView'; import { CompileScript } from '../client/util/Scripting'; +import { RichTextField } from '../new_fields/RichTextField'; import { DateField } from '../new_fields/DateField'; import { ScriptField } from '../new_fields/ScriptField'; import CursorField from '../new_fields/CursorField'; @@ -126,6 +127,8 @@ class DebugViewer extends React.Component<{ field: FieldResult, setValue(value: content = <p>"{field}"</p>; } else if (typeof field === "number" || typeof field === "boolean") { content = <p>{field}</p>; + } else if (field instanceof RichTextField) { + content = <p>RTF: {field.Data}</p>; } else if (field instanceof URLField) { content = <p>{field.url.href}</p>; } else if (field instanceof Promise) { diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 59314783b..dab0f9070 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -462,6 +462,19 @@ export namespace Doc { otherdoc.type = DocumentType.TEMPLATE; return otherdoc; } + export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetData?: Doc) { + let temp = Doc.MakeDelegate(templateDoc); + target.nativeWidth = Doc.GetProto(target).nativeWidth = undefined; + target.nativeHeight = Doc.GetProto(target).nativeHeight = undefined; + target.width = templateDoc.width; + target.height = templateDoc.height; + Doc.GetProto(target).type = DocumentType.TEMPLATE; + if (targetData && targetData.layout === target) { + targetData.layout = temp; + } else { + target.layout = temp; + } + } export function MakeTemplate(fieldTemplate: Doc, metaKey: string, templateDataDoc: Doc) { // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??) diff --git a/src/new_fields/ObjectField.ts b/src/new_fields/ObjectField.ts index 5f4a6f8fb..65ada91c0 100644 --- a/src/new_fields/ObjectField.ts +++ b/src/new_fields/ObjectField.ts @@ -1,6 +1,7 @@ import { Doc } from "./Doc"; import { RefField } from "./RefField"; import { OnUpdate, Parent, Copy, ToScriptString } from "./FieldSymbols"; +import { Scripting } from "../client/util/Scripting"; export abstract class ObjectField { protected [OnUpdate](diff?: any) { } @@ -15,3 +16,5 @@ export namespace ObjectField { return field[Copy](); } } + +Scripting.addGlobal(ObjectField);
\ No newline at end of file diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 78a3a4067..89799b2af 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -20,6 +20,6 @@ export class RichTextField extends ObjectField { } [ToScriptString]() { - return "invalid"; + return `new RichTextField("${this.Data}")`; } }
\ No newline at end of file diff --git a/src/new_fields/SchemaHeaderField.ts b/src/new_fields/SchemaHeaderField.ts index d5da56b10..23605cfb0 100644 --- a/src/new_fields/SchemaHeaderField.ts +++ b/src/new_fields/SchemaHeaderField.ts @@ -6,7 +6,7 @@ import { scriptingGlobal, Scripting } from "../client/util/Scripting"; import { ColumnType } from "../client/views/collections/CollectionSchemaView"; export const PastelSchemaPalette = new Map<string, string>([ - ["pink1", "#FFB4E8"], + // ["pink1", "#FFB4E8"], ["pink2", "#ff9cee"], ["pink3", "#ffccf9"], ["pink4", "#fcc2ff"], @@ -15,7 +15,7 @@ export const PastelSchemaPalette = new Map<string, string>([ ["purple2", "#c5a3ff"], ["purple3", "#d5aaff"], ["purple4", "#ecd4ff"], - ["purple5", "#fb34ff"], + // ["purple5", "#fb34ff"], ["purple6", "#dcd3ff"], ["purple7", "#a79aff"], ["purple8", "#b5b9ff"], @@ -25,17 +25,18 @@ export const PastelSchemaPalette = new Map<string, string>([ ["bluegreen3", "#c4faf8"], ["bluegreen4", "#85e3ff"], ["bluegreen5", "#ace7ff"], - ["bluegreen6", "#6eb5ff"], + // ["bluegreen6", "#6eb5ff"], ["bluegreen7", "#bffcc6"], ["bluegreen8", "#dbffd6"], ["yellow1", "#f3ffe3"], ["yellow2", "#e7ffac"], ["yellow3", "#ffffd1"], ["yellow4", "#fff5ba"], - ["red1", "#ffc9de"], + // ["red1", "#ffc9de"], ["red2", "#ffabab"], ["red3", "#ffbebc"], ["red4", "#ffcbc1"], + ["orange1", "#ffd5b3"], ]); export const RandomPastel = () => Array.from(PastelSchemaPalette.values())[Math.floor(Math.random() * PastelSchemaPalette.size)]; @@ -45,20 +46,23 @@ export const RandomPastel = () => Array.from(PastelSchemaPalette.values())[Math. export class SchemaHeaderField extends ObjectField { @serializable(primitive()) heading: string; + @serializable(primitive()) color: string; + @serializable(primitive()) type: number; + @serializable(primitive()) + width: number; + @serializable(primitive()) + desc: boolean | undefined; // boolean determines sort order, undefined when no sort - constructor(heading: string = "", color?: string, type?: ColumnType) { + constructor(heading: string = "", color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean) { super(); this.heading = heading; - this.color = color === "" || color === undefined ? RandomPastel() : color; - if (type) { - this.type = type; - } - else { - this.type = 0; - } + this.color = color; + this.type = type ? type : 0; + this.width = width ? width : -1; + this.desc = desc; } setHeading(heading: string) { @@ -76,6 +80,16 @@ export class SchemaHeaderField extends ObjectField { this[OnUpdate](); } + setWidth(width: number) { + this.width = width; + this[OnUpdate](); + } + + setDesc(desc: boolean | undefined) { + this.desc = desc; + this[OnUpdate](); + } + [Copy]() { return new SchemaHeaderField(this.heading, this.color, this.type); } diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts index d935a61af..b9ad96450 100644 --- a/src/new_fields/URLField.ts +++ b/src/new_fields/URLField.ts @@ -42,4 +42,5 @@ export abstract class URLField extends ObjectField { @scriptingGlobal @Deserializable("image") export class ImageField extends URLField { } @scriptingGlobal @Deserializable("video") export class VideoField extends URLField { } @scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { } -@scriptingGlobal @Deserializable("web") export class WebField extends URLField { }
\ No newline at end of file +@scriptingGlobal @Deserializable("web") export class WebField extends URLField { } +@scriptingGlobal @Deserializable("youtube") export class YoutubeField extends URLField { } diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py index 1ff0e3b31..f0f45d8f9 100644 --- a/src/scraping/buxton/scraper.py +++ b/src/scraping/buxton/scraper.py @@ -236,7 +236,7 @@ def parse_document(file_name: str): view_guids.append(write_image(pure_name, image)) copyfile(dir_path + "/" + image, dir_path + "/" + image.replace(".", "_o.", 1)) - os.rename(dir_path + "/" + image, dir_path + + copyfile(dir_path + "/" + image, dir_path + "/" + image.replace(".", "_m.", 1)) print(f"extracted {count} images...") diff --git a/src/server/Message.ts b/src/server/Message.ts index 19e0a48aa..aaee143e8 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -24,6 +24,16 @@ export interface Transferable { readonly data?: any; } +export enum YoutubeQueryTypes { + Channels, SearchVideo, VideoDetails +} + +export interface YoutubeQueryInput { + readonly type: YoutubeQueryTypes; + readonly userInput?: string; + readonly videoIds?: string; +} + export interface Reference { readonly id: string; } @@ -45,6 +55,7 @@ export namespace MessageStore { export const GetRefFields = new Message<string[]>("Get Ref Fields"); export const UpdateField = new Message<Diff>("Update Ref Field"); export const CreateField = new Message<Reference>("Create Ref Field"); + export const YoutubeApiQuery = new Message<YoutubeQueryInput>("Youtube Api Query"); export const DeleteField = new Message<string>("Delete field"); export const DeleteFields = new Message<string[]>("Delete fields"); } diff --git a/src/server/database.ts b/src/server/database.ts index 7f5331998..a7254fb0c 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -17,7 +17,7 @@ export class Database { }); } - public update(id: string, value: any, callback: () => void, upsert = true, collectionName = Database.DocumentsCollection) { + public update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) { if (this.db) { let collection = this.db.collection(collectionName); const prom = this.currentWrites[id]; @@ -30,7 +30,7 @@ export class Database { delete this.currentWrites[id]; } resolve(); - callback(); + callback(err, res); }); }); }; @@ -41,6 +41,30 @@ export class Database { } } + public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) { + if (this.db) { + let collection = this.db.collection(collectionName); + const prom = this.currentWrites[id]; + let newProm: Promise<void>; + const run = (): Promise<void> => { + return new Promise<void>(resolve => { + collection.replaceOne({ _id: id }, value, { upsert } + , (err, res) => { + if (this.currentWrites[id] === newProm) { + delete this.currentWrites[id]; + } + resolve(); + callback(err, res); + }); + }); + }; + newProm = prom ? prom.then(run) : run(); + this.currentWrites[id] = newProm; + } else { + this.onConnect.push(() => this.replace(id, value, callback, upsert, collectionName)); + } + } + public delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>; public delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>; public delete(id: any, collectionName = Database.DocumentsCollection) { @@ -126,6 +150,34 @@ export class Database { } } + public async visit(ids: string[], fn: (result: any) => string[], collectionName = "newDocuments"): Promise<void> { + if (this.db) { + const visited = new Set<string>(); + while (ids.length) { + const count = Math.min(ids.length, 1000); + const index = ids.length - count; + const fetchIds = ids.splice(index, count).filter(id => !visited.has(id)); + if (!fetchIds.length) { + continue; + } + const docs = await new Promise<{ [key: string]: any }[]>(res => Database.Instance.getDocuments(fetchIds, res, "newDocuments")); + for (const doc of docs) { + const id = doc.id; + visited.add(id); + ids.push(...fn(doc)); + } + } + + } else { + return new Promise(res => { + this.onConnect.push(() => { + this.visit(ids, fn, collectionName); + res(); + }); + }); + } + } + public query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName = "newDocuments"): Promise<mongodb.Cursor> { if (this.db) { let cursor = this.db.collection(collectionName).find(query); diff --git a/src/server/index.ts b/src/server/index.ts index adf218be6..cb46698df 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -25,8 +25,9 @@ import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLo import { DashUserModel } from './authentication/models/user_model'; import { Client } from './Client'; import { Database } from './database'; -import { MessageStore, Transferable, Types, Diff, Message } from "./Message"; +import { MessageStore, Transferable, Types, Diff, YoutubeQueryTypes as YoutubeQueryType, YoutubeQueryInput } from "./Message"; import { RouteStore } from './RouteStore'; +import v4 = require('uuid/v4'); const app = express(); const config = require('../../webpack.config'); import { createCanvas, loadImage, Canvas } from "canvas"; @@ -39,12 +40,20 @@ import c = require("crypto"); import { Search } from './Search'; import { debug } from 'util'; import _ = require('lodash'); +import * as Archiver from 'archiver'; +import * as AdmZip from 'adm-zip'; +import * as YoutubeApi from './youtubeApi/youtubeApiSample.js'; import { Response } from 'express-serve-static-core'; +import { DocComponent } from '../client/views/DocComponent'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); +var SolrNode = require('solr-node'); +var shell = require('shelljs'); const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); +let youtubeApiKey: string; +YoutubeApi.readApiKey((apiKey: string) => youtubeApiKey = apiKey); const release = process.env.RELEASE === "true"; if (process.env.RELEASE === "true") { @@ -150,6 +159,7 @@ app.get("/version", (req, res) => { }); // SEARCH +const solrURL = "http://localhost:8983/solr/#/dash"; // GETTERS @@ -177,6 +187,173 @@ function msToTime(duration: number) { return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds; } +async function getDocs(id: string) { + const files = new Set<string>(); + const docs: { [id: string]: any } = {}; + const fn = (doc: any): string[] => { + const id = doc.id; + if (typeof id === "string" && id.endsWith("Proto")) { + //Skip protos + return []; + } + const ids: string[] = []; + for (const key in doc.fields) { + if (!doc.fields.hasOwnProperty(key)) { + continue; + } + const field = doc.fields[key]; + if (field === undefined || field === null) { + continue; + } + + if (field.__type === "proxy" || field.__type === "prefetch_proxy") { + ids.push(field.fieldId); + } else if (field.__type === "script" || field.__type === "computed") { + if (field.captures) { + ids.push(field.captures.fieldId); + } + } else if (field.__type === "list") { + ids.push(...fn(field)); + } else if (typeof field === "string") { + const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w\-]*)"/g; + let match: string[] | null; + while ((match = re.exec(field)) !== null) { + ids.push(match[1]); + } + } else if (field.__type === "RichTextField") { + const re = /"href"\s*:\s*"(.*?)"/g; + let match: string[] | null; + while ((match = re.exec(field.Data)) !== null) { + const urlString = match[1]; + const split = new URL(urlString).pathname.split("doc/"); + if (split.length > 1) { + ids.push(split[split.length - 1]); + } + } + const re2 = /"src"\s*:\s*"(.*?)"/g; + while ((match = re2.exec(field.Data)) !== null) { + const urlString = match[1]; + const pathname = new URL(urlString).pathname; + files.add(pathname); + } + } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) { + const url = new URL(field.url); + const pathname = url.pathname; + files.add(pathname); + } + } + + if (doc.id) { + docs[doc.id] = doc; + } + return ids; + }; + await Database.Instance.visit([id], fn); + return { id, docs, files }; +} +app.get("/serializeDoc/:docId", async (req, res) => { + const { docs, files } = await getDocs(req.params.docId); + res.send({ docs, files: Array.from(files) }); +}); + +app.get("/downloadId/:docId", async (req, res) => { + res.set('Content-disposition', `attachment;`); + res.set('Content-Type', "application/zip"); + const { id, docs, files } = await getDocs(req.params.docId); + const docString = JSON.stringify({ id, docs }); + const zip = Archiver('zip'); + zip.pipe(res); + zip.append(docString, { name: "doc.json" }); + files.forEach(val => { + zip.file(__dirname + RouteStore.public + val, { name: val.substring(1) }); + }); + zip.finalize(); +}); + +app.post("/uploadDoc", (req, res) => { + let form = new formidable.IncomingForm(); + form.keepExtensions = true; + // let path = req.body.path; + const ids: { [id: string]: string } = {}; + let remap = true; + const getId = (id: string): string => { + if (!remap) return id; + if (id.endsWith("Proto")) return id; + if (id in ids) { + return ids[id]; + } else { + return ids[id] = v4(); + } + }; + const mapFn = (doc: any) => { + if (doc.id) { + doc.id = getId(doc.id); + } + for (const key in doc.fields) { + if (!doc.fields.hasOwnProperty(key)) { + continue; + } + const field = doc.fields[key]; + if (field === undefined || field === null) { + continue; + } + + if (field.__type === "proxy" || field.__type === "prefetch_proxy") { + field.fieldId = getId(field.fieldId); + } else if (field.__type === "script" || field.__type === "computed") { + if (field.captures) { + field.captures.fieldId = getId(field.captures.fieldId); + } + } else if (field.__type === "list") { + mapFn(field); + } else if (typeof field === "string") { + const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g; + doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => { + return `${p1}${getId(p2)}"`; + }); + } else if (field.__type === "RichTextField") { + const re = /("href"\s*:\s*")(.*?)"/g; + field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => { + return `${p1}${getId(p2)}"`; + }); + } + } + }; + form.parse(req, async (err, fields, files) => { + remap = fields.remap !== "false"; + let id: string = ""; + try { + for (const name in files) { + const path = files[name].path; + const zip = new AdmZip(path); + zip.getEntries().forEach(entry => { + if (!entry.name.startsWith("files/")) return; + zip.extractEntryTo(entry.name, __dirname + RouteStore.public, true, false); + }); + const json = zip.getEntry("doc.json"); + let docs: any; + try { + let data = JSON.parse(json.getData().toString("utf8")); + docs = data.docs; + id = data.id; + docs = Object.keys(docs).map(key => docs[key]); + docs.forEach(mapFn); + await Promise.all(docs.map((doc: any) => new Promise(res => Database.Instance.replace(doc.id, doc, (err, r) => { + err && console.log(err); + res(); + }, true, "newDocuments")))); + } catch (e) { console.log(e); } + fs.unlink(path, () => { }); + } + if (id) { + res.send(JSON.stringify(getId(id))); + } else { + res.send(JSON.stringify("error")); + } + } catch (e) { console.log(e); } + }); +}); + app.get("/whosOnline", (req, res) => { let users: any = { active: {}, inactive: {} }; const now = Date.now(); @@ -507,6 +684,7 @@ server.on("connection", function (socket: Socket) { } Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField); + Utils.AddServerHandlerCallback(socket, MessageStore.YoutubeApiQuery, HandleYoutubeQuery); Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff)); Utils.AddServerHandler(socket, MessageStore.DeleteField, id => DeleteField(socket, id)); Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids)); @@ -561,6 +739,17 @@ function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => v Database.Instance.getDocuments(ids, callback, "newDocuments"); } +function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) { + switch (query.type) { + case YoutubeQueryType.Channels: + YoutubeApi.authorizedGetChannel(youtubeApiKey); + break; + case YoutubeQueryType.SearchVideo: + YoutubeApi.authorizedGetVideos(youtubeApiKey, query.userInput, callback); + case YoutubeQueryType.VideoDetails: + YoutubeApi.authorizedGetVideoDetails(youtubeApiKey, query.videoIds, callback); + } +} const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", @@ -660,4 +849,5 @@ function CreateField(newValue: any) { } server.listen(serverPort); -console.log(`listening on port ${serverPort}`);
\ No newline at end of file +console.log(`listening on port ${serverPort}`); + diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts index 6f4d6642f..69c766d56 100644 --- a/src/server/remapUrl.ts +++ b/src/server/remapUrl.ts @@ -6,7 +6,8 @@ const suffixMap: { [type: string]: true } = { "video": true, "pdf": true, "audio": true, - "web": true + "web": true, + "image": true }; async function update() { @@ -30,10 +31,10 @@ async function update() { const value = fields[key]; if (value && value.__type && suffixMap[value.__type]) { const url = new URL(value.url); - if (url.href.includes("azure")) { + if (url.href.includes("localhost") && url.href.includes("Bill")) { dynfield = true; - update.$set = { ["fields." + key + ".url"]: `${url.protocol}//localhost:1050${url.pathname}` }; + update.$set = { ["fields." + key + ".url"]: `${url.protocol}//dash-web.eastus2.cloudapp.azure.com:1050${url.pathname}` }; } } } diff --git a/src/server/youtubeApi/youtubeApiSample.d.ts b/src/server/youtubeApi/youtubeApiSample.d.ts new file mode 100644 index 000000000..427f54608 --- /dev/null +++ b/src/server/youtubeApi/youtubeApiSample.d.ts @@ -0,0 +1,2 @@ +declare const YoutubeApi: any; +export = YoutubeApi;
\ No newline at end of file diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/youtubeApi/youtubeApiSample.js new file mode 100644 index 000000000..50b3c7b38 --- /dev/null +++ b/src/server/youtubeApi/youtubeApiSample.js @@ -0,0 +1,179 @@ +const fs = require('fs'); +const readline = require('readline'); +const { google } = require('googleapis'); +const OAuth2 = google.auth.OAuth2; + + +// If modifying these scopes, delete your previously saved credentials +// at ~/.credentials/youtube-nodejs-quickstart.json +let SCOPES = ['https://www.googleapis.com/auth/youtube.readonly']; +let TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH || + process.env.USERPROFILE) + '/.credentials/'; +let TOKEN_PATH = TOKEN_DIR + 'youtube-nodejs-quickstart.json'; + +module.exports.readApiKey = (callback) => { + fs.readFile('client_secret.json', function processClientSecrets(err, content) { + if (err) { + console.log('Error loading client secret file: ' + err); + return; + } + callback(content); + }); +} + +module.exports.authorizedGetChannel = (apiKey) => { + //this didnt get called + // Authorize a client with the loaded credentials, then call the YouTube API. + authorize(JSON.parse(apiKey), getChannel); +} + +module.exports.authorizedGetVideos = (apiKey, userInput, callBack) => { + authorize(JSON.parse(apiKey), getVideos, { userInput: userInput, callBack: callBack }); +} + +module.exports.authorizedGetVideoDetails = (apiKey, videoIds, callBack) => { + authorize(JSON.parse(apiKey), getVideoDetails, { videoIds: videoIds, callBack: callBack }); +} + + +/** + * Create an OAuth2 client with the given credentials, and then execute the + * given callback function. + * + * @param {Object} credentials The authorization client credentials. + * @param {function} callback The callback to call with the authorized client. + */ +function authorize(credentials, callback, args = {}) { + let clientSecret = credentials.installed.client_secret; + let clientId = credentials.installed.client_id; + let redirectUrl = credentials.installed.redirect_uris[0]; + let oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl); + + // Check if we have previously stored a token. + fs.readFile(TOKEN_PATH, function (err, token) { + if (err) { + getNewToken(oauth2Client, callback); + } else { + oauth2Client.credentials = JSON.parse(token); + callback(oauth2Client, args); + } + }); +} + +/** + * Get and store new token after prompting for user authorization, and then + * execute the given callback with the authorized OAuth2 client. + * + * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for. + * @param {getEventsCallback} callback The callback to call with the authorized + * client. + */ +function getNewToken(oauth2Client, callback) { + var authUrl = oauth2Client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES + }); + console.log('Authorize this app by visiting this url: ', authUrl); + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + rl.question('Enter the code from that page here: ', function (code) { + rl.close(); + oauth2Client.getToken(code, function (err, token) { + if (err) { + console.log('Error while trying to retrieve access token', err); + return; + } + oauth2Client.credentials = token; + storeToken(token); + callback(oauth2Client); + }); + }); +} + +/** + * Store token to disk be used in later program executions. + * + * @param {Object} token The token to store to disk. + */ +function storeToken(token) { + try { + fs.mkdirSync(TOKEN_DIR); + } catch (err) { + if (err.code != 'EEXIST') { + throw err; + } + } + fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => { + if (err) throw err; + console.log('Token stored to ' + TOKEN_PATH); + }); + console.log('Token stored to ' + TOKEN_PATH); +} + +/** + * Lists the names and IDs of up to 10 files. + * + * @param {google.auth.OAuth2} auth An authorized OAuth2 client. + */ +function getChannel(auth) { + var service = google.youtube('v3'); + service.channels.list({ + auth: auth, + part: 'snippet,contentDetails,statistics', + forUsername: 'GoogleDevelopers' + }, function (err, response) { + if (err) { + console.log('The API returned an error: ' + err); + return; + } + var channels = response.data.items; + if (channels.length == 0) { + console.log('No channel found.'); + } else { + console.log('This channel\'s ID is %s. Its title is \'%s\', and ' + + 'it has %s views.', + channels[0].id, + channels[0].snippet.title, + channels[0].statistics.viewCount); + } + }); +} + +function getVideos(auth, args) { + let service = google.youtube('v3'); + service.search.list({ + auth: auth, + part: 'id, snippet', + type: 'video', + q: args.userInput, + maxResults: 10 + }, function (err, response) { + if (err) { + console.log('The API returned an error: ' + err); + return; + } + let videos = response.data.items; + args.callBack(videos); + }); +} + +function getVideoDetails(auth, args) { + if (args.videoIds === undefined) { + return; + } + let service = google.youtube('v3'); + service.videos.list({ + auth: auth, + part: 'contentDetails, statistics', + id: args.videoIds + }, function (err, response) { + if (err) { + console.log('The API returned an error from details: ' + err); + return; + } + let videoDetails = response.data.items; + args.callBack(videoDetails); + }); +}
\ No newline at end of file |