diff options
Diffstat (limited to 'src')
53 files changed, 1361 insertions, 463 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex d70e95c0a..ff00f097e 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 6dc98dbbc..7d7a1f02a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -97,9 +97,10 @@ export namespace DocUtils { let linkDocProto = Doc.GetProto(linkDoc); linkDocProto.context = targetContext; - linkDocProto.title = title; //=== "" ? source.title + " to " + target.title : title; + linkDocProto.title = title === "" ? source.title + " to " + target.title : title; linkDocProto.linkDescription = description; linkDocProto.linkTags = tags; + linkDocProto.type = DocTypes.LINK; linkDocProto.anchor1 = source; linkDocProto.anchor1Page = source.curPage; diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index d0014dbf3..acd8dcef7 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -44,15 +44,14 @@ export class DocumentManager { DocumentManager.Instance.DocumentViews.map(view => { let doc = view.props.Document.proto; if (doc && doc[Id]) { - if(doc[Id] === id) - {toReturn.push(view);} + if (doc[Id] === id) { toReturn.push(view); } } }); } return toReturn; } - public getAllDocumentViews(doc: Doc){ + public getAllDocumentViews(doc: Doc) { return this.getDocumentViewsById(doc[Id]); } @@ -150,7 +149,7 @@ export class DocumentManager { docContext.panTransformType = "Ease"; targetContextView.props.focus(docDelegate, willZoom); } else { - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, docContext); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, undefined); setTimeout(() => { this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); }, 10); @@ -159,7 +158,7 @@ export class DocumentManager { const actualDoc = Doc.MakeAlias(docDelegate); actualDoc.libraryBrush = true; if (linkPage !== undefined) actualDoc.curPage = linkPage; - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, actualDoc); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, undefined); } } else { let contextView: DocumentView | null; @@ -168,7 +167,7 @@ export class DocumentManager { contextDoc.panTransformType = "Ease"; contextView.props.focus(docDelegate, willZoom); } else { - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, contextDoc); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, undefined); setTimeout(() => { this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); }, 10); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index a6bba3656..7dc48fb78 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,18 +1,24 @@ -import { action, runInAction, observable } from "mobx"; -import { Doc, DocListCastAsync } from "../../new_fields/Doc"; -import { Cast, StrCast } from "../../new_fields/Types"; +import { action, runInAction } from "mobx"; +import { Doc } from "../../new_fields/Doc"; +import { Cast } from "../../new_fields/Types"; +import { URLField } from "../../new_fields/URLField"; import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import * as globalCssVariables from "../views/globalCssVariables.scss"; +import { DocumentManager } from "./DocumentManager"; import { LinkManager } from "./LinkManager"; -import { URLField } from "../../new_fields/URLField"; import { SelectionManager } from "./SelectionManager"; -import { Docs, DocUtils } from "../documents/Documents"; -import { DocumentManager } from "./DocumentManager"; -import { Id } from "../../new_fields/FieldSymbols"; export type dropActionType = "alias" | "copy" | undefined; -export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, options?: any, dontHideOnDrop?: boolean) { +export function SetupDrag( + _reference: React.RefObject<HTMLElement>, + docFunc: () => Doc | Promise<Doc>, + moveFunc?: DragManager.MoveFunction, + dropAction?: dropActionType, + options?: any, + dontHideOnDrop?: boolean, + dragStarted?: () => void +) { let onRowMove = async (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); @@ -20,12 +26,13 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); let doc = await docFunc(); - var dragData = new DragManager.DocumentDragData([doc], [doc]); + var dragData = new DragManager.DocumentDragData([doc], [undefined]); dragData.dropAction = dropAction; dragData.moveDocument = moveFunc; dragData.options = options; dragData.dontHideOnDrop = dontHideOnDrop; DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y); + dragStarted && dragStarted(); }; let onRowUp = (): void => { document.removeEventListener("pointermove", onRowMove); @@ -34,6 +41,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () let onItemDown = async (e: React.PointerEvent) => { if (e.button === 0) { e.stopPropagation(); + e.preventDefault(); if (e.shiftKey && CollectionDockingView.Instance) { CollectionDockingView.Instance.StartOtherDrag(e, [await docFunc()]); } else { @@ -111,6 +119,8 @@ export namespace DragManager { hideSource: boolean | (() => boolean); + dragHasStarted?: () => void; + withoutShiftDrag?: boolean; } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 7dbb81e76..3bc71ad42 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,11 +1,13 @@ -import { observable, action, runInAction } from "mobx"; +import { observable, action, runInAction, IReactionDisposer, reaction, autorun } from "mobx"; import { Doc } from "../../new_fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; import { NumCast } from "../../new_fields/Types"; export namespace SelectionManager { + class Manager { + @observable IsDragging: boolean = false; @observable SelectedDocuments: Array<DocumentView> = []; @@ -18,6 +20,7 @@ export namespace SelectionManager { } manager.SelectedDocuments.push(docView); + // console.log(manager.SelectedDocuments); docView.props.whenActiveChanged(true); } } diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 5c1324d7d..e72e3cf5e 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -1,57 +1,37 @@ -import { action, IReactionDisposer, reaction } from "mobx"; -import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css -import { baseKeymap, lift, deleteSelection } from "prosemirror-commands"; -import { history, redo, undo } from "prosemirror-history"; -import { keymap } from "prosemirror-keymap"; -import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state"; +import { action } from "mobx"; +import { Dropdown, MenuItem, icons, } from "prosemirror-menu"; //no import css +import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { schema } from "./RichTextSchema"; import { Schema, NodeType, MarkType, Mark, ResolvedPos } from "prosemirror-model"; -import { Node as ProsNode } from "prosemirror-model" -import React = require("react"); +import { Node as ProsNode } from "prosemirror-model"; import "./TooltipTextMenu.scss"; -const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); +const { toggleMark, setBlockType } = require("prosemirror-commands"); import { library } from '@fortawesome/fontawesome-svg-core'; -import { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list'; -import { liftTarget, RemoveMarkStep, AddMarkStep } from 'prosemirror-transform'; -import { - faListUl, faGrinTongueSquint, -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { wrapInList, liftListItem, } from 'prosemirror-schema-list'; +import { faListUl } from '@fortawesome/free-solid-svg-icons'; import { FieldViewProps } from "../views/nodes/FieldView"; -import { throwStatement } from "babel-types"; const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js"); -import { View } from "@react-pdf/renderer"; import { DragManager } from "./DragManager"; import { Doc, Opt, Field } from "../../new_fields/Doc"; import { DocServer } from "../DocServer"; -import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { DocumentManager } from "./DocumentManager"; import { Id } from "../../new_fields/FieldSymbols"; -import { Utils } from "../../Utils"; import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox"; -import { text } from "body-parser"; -import { type } from "os"; -// import { wrap } from "module"; - -const SVG = "http://www.w3.org/2000/svg"; //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. export class TooltipTextMenu { public tooltip: HTMLElement; - private num_icons = 0; private view: EditorView; private fontStyles: MarkType[]; private fontSizes: MarkType[]; private listTypes: NodeType[]; private editorProps: FieldViewProps & FormattedTextBoxProps; - private state: EditorState; private fontSizeToNum: Map<MarkType, number>; private fontStylesToName: Map<MarkType, string>; private listTypeToIcon: Map<NodeType, string>; - private fontSizeIndicator: HTMLSpanElement = document.createElement("span"); private link: HTMLAnchorElement; //private wrapper: HTMLDivElement; @@ -69,7 +49,6 @@ export class TooltipTextMenu { constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) { this.view = view; - this.state = view.state; this.editorProps = editorProps; //this.wrapper = document.createElement("div"); this.tooltip = document.createElement("div"); @@ -285,7 +264,7 @@ export class TooltipTextMenu { if (DocumentManager.Instance.getDocumentView(f)) { DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false); } - else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f, f); + else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f, undefined); } })); } @@ -378,7 +357,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[]) => { - let { empty, $cursor, ranges } = view.state.selection as TextSelection; + let { $cursor, ranges } = view.state.selection as TextSelection; let state = view.state; let dispatch = view.dispatch; @@ -390,13 +369,12 @@ export class TooltipTextMenu { dispatch(state.tr.removeStoredMark(type)); } } else { - let has = false, tr = state.tr; + let has = false; for (let i = 0; !has && i < ranges.length; i++) { let { $from, $to } = ranges[i]; has = state.doc.rangeHasMark($from.pos, $to.pos, type); } for (let i of ranges) { - let { $from, $to } = i; if (has) { toggleMark(type)(view.state, view.dispatch, view); } @@ -418,7 +396,7 @@ export class TooltipTextMenu { } //remove all node typeand apply the passed-in one to the selected text - changeToNodeType(nodeType: NodeType | undefined, view: EditorView, allNodes: NodeType[]) { + changeToNodeType(nodeType: NodeType | undefined, view: EditorView) { //remove old liftListItem(schema.nodes.list_item)(view.state, view.dispatch); if (nodeType) { //add new @@ -435,7 +413,7 @@ export class TooltipTextMenu { execEvent: "", class: "menuicon", css: css, - enable(state) { return true; }, + enable() { return true; }, run() { changeToMarkInGroup(markType, view, groupMarks); } @@ -450,7 +428,7 @@ export class TooltipTextMenu { css: "color:white;", class: "summarize", execEvent: "", - run: (state, dispatch, view) => { + run: (state, dispatch) => { TooltipTextMenu.insertStar(state, dispatch); } @@ -465,7 +443,7 @@ export class TooltipTextMenu { execEvent: "", css: "color:white;", class: "summarize", - run: (state, dispatch, view) => { + run: () => { this.collapseToolTip(); } }); @@ -544,7 +522,7 @@ export class TooltipTextMenu { execEvent: "", class: "menuicon", css: css, - enable(state) { return true; }, + enable() { return true; }, run() { changeToNodeInGroup(nodeType, view, groupNodes); } @@ -663,17 +641,6 @@ export class TooltipTextMenu { //return; } - //let linksInSelection = this.activeMarksOnSelection([schema.marks.link]); - // if (linksInSelection.length > 0) { - // let attributes = this.getMarksInSelection(this.view.state, [schema.marks.link])[0].attrs; - // this.link.href = attributes.href; - // this.link.textContent = attributes.title; - // this.link.style.visibility = "visible"; - // } else this.link.style.visibility = "hidden"; - - // Otherwise, reposition it and update its content - //this.tooltip.style.display = ""; - let { from, to } = state.selection; //UPDATE LIST ITEM DROPDOWN @@ -720,17 +687,16 @@ export class TooltipTextMenu { //finds all active marks on selection in given group activeMarksOnSelection(markGroup: MarkType[]) { //current selection - let { empty, $cursor, ranges } = this.view.state.selection as TextSelection; + let { empty, ranges } = this.view.state.selection as TextSelection; let state = this.view.state; let dispatch = this.view.dispatch; let activeMarks: MarkType[]; if (!empty) { activeMarks = markGroup.filter(mark => { if (dispatch) { - let has = false, tr = state.tr; + let has = false; for (let i = 0; !has && i < ranges.length; i++) { let { $from, $to } = ranges[i]; - let hasmark: boolean = state.doc.rangeHasMark($from.pos, $to.pos, mark); return state.doc.rangeHasMark($from.pos, $to.pos, mark); } } @@ -741,9 +707,7 @@ export class TooltipTextMenu { const pos = this.view.state.selection.$from; const ref_node: ProsNode = this.reference_node(pos); if (ref_node !== null && ref_node !== this.view.state.doc) { - let text_node_type: NodeType; if (ref_node.isText) { - text_node_type = ref_node.type; } else { return []; @@ -778,7 +742,7 @@ export class TooltipTextMenu { else if (pos.pos > 0) { let skip = false; for (let i: number = pos.pos - 1; i > 0; i--) { - this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode, pos: number, parent: ProsNode, index: number) => { + this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => { if (node.isLeaf && !skip) { ref_node = node; skip = true; diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.js index 0f9328872..f6fe1068a 100644 --- a/src/client/util/request-image-size.js +++ b/src/client/util/request-image-size.js @@ -10,7 +10,7 @@ */ const request = require('request'); -const imageSize = require('image-size'); +// const imageSize = require('image-size'); const HttpError = require('standard-http-error'); module.exports = function requestImageSize(options) { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index db10007f4..61e9d209a 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -26,6 +26,7 @@ import { Template, Templates } from "./Templates"; import React = require("react"); import { RichTextField } from '../../new_fields/RichTextField'; import { LinkManager } from '../util/LinkManager'; +import { ObjectField } from '../../new_fields/ObjectField'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -77,32 +78,31 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> this._fieldKey = text.slice(1, text.length); this._title = this.selectionTitle; } else if (text.startsWith(">")) { - let field = SelectionManager.SelectedDocuments()[0]; - let collection = field.props.ContainingCollectionView!.props.Document; - - let collectionKey = field.props.ContainingCollectionView!.props.fieldKey; - let collectionKeyProp = `fieldKey={"${collectionKey}"}`; + let fieldTemplateView = SelectionManager.SelectedDocuments()[0]; + SelectionManager.DeselectAll(); + let fieldTemplate = fieldTemplateView.props.Document; + let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document; let metaKey = text.slice(1, text.length); - let metaKeyProp = `fieldKey={"${metaKey}"}`; - - let template = Doc.MakeAlias(field.props.Document); - template.proto = collection; - template.title = metaKey; - template.nativeWidth = Cast(field.nativeWidth, "number"); - template.nativeHeight = Cast(field.nativeHeight, "number"); - template.embed = true; - template.isTemplate = true; - template.templates = new List<string>([Templates.TitleBar(metaKey)]); - if (field.props.Document.backgroundLayout) { - let metaAnoKeyProp = `fieldKey={"${metaKey}"} fieldExt={"annotations"}`; - let collectionAnoKeyProp = `fieldKey={"annotations"}`; - template.layout = StrCast(field.props.Document.layout).replace(collectionAnoKeyProp, metaAnoKeyProp); - template.backgroundLayout = StrCast(field.props.Document.backgroundLayout).replace(collectionKeyProp, metaKeyProp); - } else { - template.layout = StrCast(field.props.Document.layout).replace(collectionKeyProp, metaKeyProp); + + // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??) + let backgroundLayout = StrCast(fieldTemplate.backgroundLayout); + let layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`); + if (backgroundLayout) { + layout = StrCast(fieldTemplate.layout).replace(/fieldKey={"annotations"}/, `fieldKey={"${metaKey}"} fieldExt={"annotations"}`); + backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`); } - Doc.AddDocToList(collection, collectionKey, template); - SelectionManager.SelectedDocuments().map(dv => dv.props.removeDocument && dv.props.removeDocument(dv.props.Document)); + let nw = Cast(fieldTemplate.nativeWidth, "number"); + let nh = Cast(fieldTemplate.nativeHeight, "number"); + + fieldTemplate.title = metaKey; + fieldTemplate.layout = layout; + fieldTemplate.backgroundLayout = backgroundLayout; + fieldTemplate.nativeWidth = nw; + fieldTemplate.nativeHeight = nh; + fieldTemplate.embed = true; + fieldTemplate.isTemplate = true; + fieldTemplate.templates = new List<string>([Templates.TitleBar(metaKey)]); + fieldTemplate.proto = Doc.GetProto(docTemplate); } else { if (SelectionManager.SelectedDocuments().length > 0) { @@ -500,7 +500,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let nheight = doc.nativeHeight || 0; let width = (doc.width || 0); let height = (doc.height || (nheight / nwidth * width)); - let scale = element.props.ScreenToLocalTransform().Scale; + let scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling(); let actualdW = Math.max(width + (dW * scale), 20); let actualdH = Math.max(height + (dH * scale), 20); doc.x = (doc.x || 0) + dX * (actualdW - width); diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts new file mode 100644 index 000000000..e467d7c61 --- /dev/null +++ b/src/client/views/GlobalKeyHandler.ts @@ -0,0 +1,145 @@ +import { UndoManager } from "../util/UndoManager"; +import { SelectionManager } from "../util/SelectionManager"; +import { CollectionDockingView } from "./collections/CollectionDockingView"; +import { MainView } from "./MainView"; +import { DragManager } from "../util/DragManager"; +import { action } from "mobx"; + +const modifiers = ["control", "meta", "shift", "alt"]; +type KeyHandler = (keycode: string) => KeyControlInfo; +type KeyControlInfo = { + preventDefault: boolean, + stopPropagation: boolean +}; + +export default class KeyManager { + public static Handler: KeyManager = new KeyManager(); + private router = new Map<string, KeyHandler>(); + + constructor() { + let isMac = navigator.platform.toLowerCase().indexOf("mac") >= 0; + + // SHIFT CONTROL ALT META + this.router.set("0000", this.unmodified); + this.router.set(isMac ? "0001" : "0100", this.ctrl); + this.router.set(isMac ? "0100" : "0010", this.alt); + this.router.set(isMac ? "1001" : "1100", this.ctrl_shift); + } + + public handle = (e: KeyboardEvent) => { + let keyname = e.key.toLowerCase(); + this.handleGreedy(keyname); + + if (modifiers.includes(keyname)) { + return; + } + + let bit = (value: boolean) => value ? "1" : "0"; + let modifierIndex = bit(e.shiftKey) + bit(e.ctrlKey) + bit(e.altKey) + bit(e.metaKey); + + let handleConstrained = this.router.get(modifierIndex); + if (!handleConstrained) { + return; + } + + let control = handleConstrained(keyname); + + control.stopPropagation && e.stopPropagation(); + control.preventDefault && e.preventDefault(); + } + + private handleGreedy = action((keyname: string) => { + switch (keyname) { + } + }); + + private unmodified = action((keyname: string) => { + switch (keyname) { + case "escape": + if (MainView.Instance.isPointerDown) { + DragManager.AbortDrag(); + } else { + if (CollectionDockingView.Instance.HasFullScreen()) { + CollectionDockingView.Instance.CloseFullScreen(); + } else { + SelectionManager.DeselectAll(); + } + } + break; + } + + return { + stopPropagation: false, + preventDefault: false + }; + }); + + private alt = action((keyname: string) => { + let stopPropagation = true; + let preventDefault = true; + + switch (keyname) { + case "n": + let toggle = MainView.Instance.addMenuToggle.current!; + toggle.checked = !toggle.checked; + break; + } + + return { + stopPropagation: stopPropagation, + preventDefault: preventDefault + }; + }); + + private ctrl = action((keyname: string) => { + let stopPropagation = true; + let preventDefault = true; + + switch (keyname) { + case "arrowright": + MainView.Instance.mainFreeform && CollectionDockingView.Instance.AddRightSplit(MainView.Instance.mainFreeform, undefined); + break; + case "arrowleft": + MainView.Instance.mainFreeform && CollectionDockingView.Instance.CloseRightSplit(MainView.Instance.mainFreeform); + break; + case "f": + MainView.Instance.isSearchVisible = !MainView.Instance.isSearchVisible; + break; + case "o": + let target = SelectionManager.SelectedDocuments()[0]; + target && target.fullScreenClicked(); + break; + case "r": + preventDefault = false; + break; + case "y": + UndoManager.Redo(); + break; + case "z": + UndoManager.Undo(); + break; + } + + return { + stopPropagation: stopPropagation, + preventDefault: preventDefault + }; + }); + + private ctrl_shift = action((keyname: string) => { + let stopPropagation = true; + let preventDefault = true; + + switch (keyname) { + case "z": + UndoManager.Redo(); + break; + } + + return { + stopPropagation: stopPropagation, + preventDefault: preventDefault + }; + }); + +}
\ No newline at end of file diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index 1933ab94b..d31319429 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -108,7 +108,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> document.removeEventListener('pointerup', this.textBoxUp); } - addDocTab = (doc: Doc, dataDoc: Doc, location: string) => { + addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => { if (true) { // location === "onRight") { need to figure out stack to add "inTab" CollectionDockingView.Instance.AddRightSplit(doc, dataDoc); } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 6290d8985..087bc904a 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -36,24 +36,24 @@ import { CollectionBaseView } from './collections/CollectionBaseView'; import { List } from '../../new_fields/List'; import PDFMenu from './pdf/PDFMenu'; import { InkTool } from '../../new_fields/InkField'; -import * as _ from "lodash"; +import _ from "lodash"; +import KeyManager from './GlobalKeyHandler'; @observer export class MainView extends React.Component { public static Instance: MainView; + @observable addMenuToggle = React.createRef<HTMLInputElement>(); @observable private _workspacesShown: boolean = false; @observable public pwidth: number = 0; @observable public pheight: number = 0; @computed private get mainContainer(): Opt<Doc> { return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc)); } - @computed private get mainFreeform(): Opt<Doc> { + @computed get mainFreeform(): Opt<Doc> { let docs = DocListCast(this.mainContainer!.data); return (docs && docs.length > 1) ? docs[1] : undefined; } - private globalDisplayFlags = observable({ - jumpToVisible: false - }); + public isPointerDown = false; private set mainContainer(doc: Opt<Doc>) { if (doc) { if (!("presentationView" in doc)) { @@ -64,12 +64,23 @@ export class MainView extends React.Component { } componentWillMount() { - document.removeEventListener("keydown", this.globalKeyHandler); - document.addEventListener("keydown", this.globalKeyHandler); + window.removeEventListener("keydown", KeyManager.Handler.handle); + window.addEventListener("keydown", KeyManager.Handler.handle); + + window.removeEventListener("pointerdown", this.pointerDown); + window.addEventListener("pointerdown", this.pointerDown); + + window.removeEventListener("pointerup", this.pointerUp); + window.addEventListener("pointerup", this.pointerUp); } + pointerDown = (e: PointerEvent) => this.isPointerDown = true; + pointerUp = (e: PointerEvent) => this.isPointerDown = false; + componentWillUnMount() { - document.removeEventListener("keydown", this.globalKeyHandler); + window.removeEventListener("keydown", KeyManager.Handler.handle); + window.removeEventListener("pointerdown", this.pointerDown); + window.removeEventListener("pointerup", this.pointerUp); } constructor(props: Readonly<{}>) { @@ -122,18 +133,6 @@ export class MainView extends React.Component { // window.addEventListener("pointermove", (e) => this.reportLocation(e)) window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler - window.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - DragManager.AbortDrag(); - SelectionManager.DeselectAll(); - } else if (e.key === "z" && e.ctrlKey) { - e.preventDefault(); - UndoManager.Undo(); - } else if ((e.key === "y" && e.ctrlKey) || (e.key === "z" && e.ctrlKey && e.shiftKey)) { - e.preventDefault(); - UndoManager.Redo(); - } - }, false); // drag event handler // click interactions for the context menu document.addEventListener("pointerdown", action(function (e: PointerEvent) { @@ -203,7 +202,7 @@ export class MainView extends React.Component { openNotifsCol = () => { if (this._notifsCol && CollectionDockingView.Instance) { - CollectionDockingView.Instance.AddRightSplit(this._notifsCol, this._notifsCol); + CollectionDockingView.Instance.AddRightSplit(this._notifsCol, undefined); } } @@ -292,7 +291,7 @@ export class MainView extends React.Component { ]; return < div id="add-nodes-menu" > - <input type="checkbox" id="add-menu-toggle" /> + <input type="checkbox" id="add-menu-toggle" ref={this.addMenuToggle} /> <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label> <div id="add-options-content"> @@ -300,8 +299,7 @@ export class MainView extends React.Component { <li key="search"><button className="add-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button></li> <li key="undo"><button className="add-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button></li> <li key="redo"><button className="add-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button></li> - <li key="color"><button className="add-button round-button" title="Redo" onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} > - + <li key="color"><button className="add-button round-button" title="Select Color" onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} > <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}> <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} /> </div> @@ -359,41 +357,10 @@ export class MainView extends React.Component { @observable isSearchVisible = false; @action toggleSearch = () => { + // console.log("search toggling") this.isSearchVisible = !this.isSearchVisible; } - @action - globalKeyHandler = (e: KeyboardEvent) => { - if (e.key === "Control" || !e.ctrlKey) return; - - if (e.key === "v") return; - if (e.key === "c") return; - - e.preventDefault(); - e.stopPropagation(); - - switch (e.key) { - case "ArrowRight": - if (this.mainFreeform) { - CollectionDockingView.Instance.AddRightSplit(this.mainFreeform, undefined); - } - break; - case "ArrowLeft": - if (this.mainFreeform) { - CollectionDockingView.Instance.CloseRightSplit(this.mainFreeform); - } - break; - case "o": - this.globalDisplayFlags.jumpToVisible = true; - break; - case "escape": - _.mapValues(this.globalDisplayFlags, () => false); - break; - case "f": - this.isSearchVisible = !this.isSearchVisible; - } - } - render() { return ( 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/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index e675e82fc..13b0579d2 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -24,7 +24,6 @@ import { SubCollectionViewProps } from "./CollectionSubView"; import { ParentDocSelector } from './ParentDocumentSelector'; import React = require("react"); import { MainView } from '../MainView'; -import { LinkManager } from '../../util/LinkManager'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { library } from '@fortawesome/fontawesome-svg-core'; import { faFile } from '@fortawesome/free-solid-svg-icons'; @@ -52,6 +51,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp private _flush: boolean = false; private _ignoreStateChange = ""; private _isPointerDown = false; + private _maximizedSrc: Opt<DocumentView>; constructor(props: SubCollectionViewProps) { super(props); @@ -66,12 +66,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this.hack = true; this.undohack = UndoManager.StartBatch("goldenDrag"); dragDocs.map((dragDoc, i) => - this.AddRightSplit(dragDoc, dragDataDocs ? dragDataDocs[i] : dragDoc, true).contentItems[0].tab._dragListener. + this.AddRightSplit(dragDoc, dragDataDocs ? dragDataDocs[i] : undefined, true).contentItems[0].tab._dragListener. onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); } @action - public OpenFullScreen(document: Doc, dataDoc: Doc) { + public OpenFullScreen(docView: DocumentView) { + let document = Doc.MakeAlias(docView.props.Document); + let dataDoc = docView.dataDoc; let newItemStackConfig = { type: 'stack', content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] @@ -80,10 +82,25 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.root.contentItems[0].addChild(docconfig); docconfig.callDownwards('_$init'); this._goldenLayout._$maximiseItem(docconfig); + this._maximizedSrc = docView; this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); this.stateChanged(); } + public CloseFullScreen = () => { + let target = this._goldenLayout._maximisedItem; + if (target !== null && this._maximizedSrc) { + this._goldenLayout._maximisedItem.remove(); + SelectionManager.SelectDoc(this._maximizedSrc, false); + this._maximizedSrc = undefined; + this.stateChanged(); + } + } + + public HasFullScreen = () => { + return this._goldenLayout._maximisedItem !== null; + } + @undoBatch @action public CloseRightSplit = (document: Doc): boolean => { @@ -167,7 +184,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return newContentItem; } @action - public AddTab = (stack: any, document: Doc, dataDocument: Doc) => { + public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined) => { let docs = Cast(this.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); @@ -257,7 +274,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @action onPointerUp = (e: React.PointerEvent): void => { - this._isPointerDown = false; if (this._flush) { this._flush = false; setTimeout(() => this.stateChanged(), 10); @@ -350,7 +366,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc; let dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc; - if (doc instanceof Doc && dataDoc instanceof Doc) { + if (doc instanceof Doc) { let dragSpan = document.createElement("span"); dragSpan.style.position = "relative"; dragSpan.style.bottom = "6px"; @@ -469,7 +485,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { constructor(props: any) { super(props); DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => { - this._dataDoc = this._document = f as Doc; + this._document = f as Doc; if (this.props.dataDocumentId && this.props.documentId !== this.props.dataDocumentId) { DocServer.GetRefField(this.props.dataDocumentId).then(action((f: Opt<Field>) => this._dataDoc = f as Doc)); } @@ -510,7 +526,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } - addDocTab = (doc: Doc, dataDoc: Doc, location: string) => { + addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => { if (doc.dockingConfig) { MainView.Instance.openWorkspace(doc); } else if (location === "onRight") { @@ -520,15 +536,16 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } } get content() { - if (!this._document || !this._dataDoc) { + if (!this._document) { return (null); } + let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc; return ( <div className="collectionDockingView-content" ref={this._mainCont} style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier})` }}> <DocumentView key={this._document[Id]} Document={this._document} - DataDoc={this._dataDoc} + DataDoc={resolvedDataDoc} bringToFront={emptyFunction} addDocument={undefined} removeDocument={undefined} diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 98bf513bb..b0d46953c 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -119,8 +119,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { let fieldContentView = <FieldView {...props} />; let reference = React.createRef<HTMLDivElement>(); let onItemDown = (e: React.PointerEvent) => { - (this.props.CollectionView.props.isSelected() ? - SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e) : undefined); + (!this.props.CollectionView.props.isSelected() ? undefined : + SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); }; let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc }); @@ -349,12 +349,22 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; } + @computed get previewPanel() { + // let layoutDoc = this.previewDocument; + // let resolvedDataDoc = (layoutDoc !== this.props.DataDoc) ? this.props.DataDoc : undefined; + // if (layoutDoc && !(Cast(layoutDoc.layout, Doc) instanceof Doc) && + // resolvedDataDoc && resolvedDataDoc !== layoutDoc) { + // // ... so change the layout to be an expanded view of the template layout. This allows the view override the template's properties and be referenceable as its own document. + // layoutDoc = Doc.expandTemplateLayout(layoutDoc, resolvedDataDoc); + // } + + let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined; return <div ref={this.createTarget}> <CollectionSchemaPreview - Document={this.previewDocument} - DataDocument={BoolCast(this.props.Document.isTemplate) ? this.previewDocument : this.props.DataDoc} + Document={layoutDoc} + DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined} childDocs={this.childDocs} renderDepth={this.props.renderDepth} width={this.previewWidth} @@ -404,7 +414,7 @@ interface CollectionSchemaPreviewProps { removeDocument: (document: Doc) => boolean; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; - addDocTab: (document: Doc, dataDoc: Doc, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; setPreviewScript: (script: string) => void; previewScript?: string; } diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 6c4ea18a1..b10907937 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -4,12 +4,13 @@ import { action, computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; -import { BoolCast, NumCast } from "../../../new_fields/Types"; +import { BoolCast, NumCast, Cast } from "../../../new_fields/Types"; import { emptyFunction, Utils } from "../../../Utils"; import { ContextMenu } from "../ContextMenu"; import { CollectionSchemaPreview } from "./CollectionSchemaView"; import "./CollectionStackingView.scss"; import { CollectionSubView } from "./CollectionSubView"; +import { resolve } from "bluebird"; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { @@ -66,17 +67,18 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { get singleColumnChildren() { let children = this.childDocs.filter(d => !d.isMinimized); return children.map((d, i) => { + let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc); let dref = React.createRef<HTMLDivElement>(); - let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]()); + let dxf = () => this.getDocTransform(layoutDoc, dref.current!).scale(this.columnWidth / d[WidthSym]()); let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; - let height = () => this.singleColDocHeight(d); + let height = () => this.singleColDocHeight(layoutDoc); return <div className="collectionStackingView-columnDoc" key={d[Id]} ref={dref} style={{ width: width(), height: height() }} > <CollectionSchemaPreview - Document={d} - DataDocument={this.props.Document.layout instanceof Doc ? this.props.Document : this.props.DataDoc} + Document={layoutDoc} + DataDocument={d !== this.props.DataDoc ? this.props.DataDoc : undefined} renderDepth={this.props.renderDepth} width={width} height={height} diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 5c80fbd38..927aa363f 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -36,7 +36,7 @@ export interface TreeViewProps { deleteDoc: (doc: Doc) => boolean; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; - addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; panelWidth: () => number; panelHeight: () => number; addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; @@ -97,7 +97,7 @@ class TreeView extends React.Component<TreeViewProps> { } @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc); - @undoBatch openRight = async () => this.props.addDocTab(this.props.document, this.props.document, "onRight"); + @undoBatch openRight = async () => this.props.addDocTab(this.props.document, undefined, "onRight"); onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); onPointerEnter = (e: React.PointerEvent): void => { @@ -183,16 +183,19 @@ class TreeView extends React.Component<TreeViewProps> { let keys = Array.from(Object.keys(this.resolvedDataDoc)); if (this.resolvedDataDoc.proto instanceof Doc) { keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto))); - while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); } - let keyList: string[] = keys.reduce((l, key) => Cast(this.resolvedDataDoc[key], listSpec(Doc)) ? [...l, key] : l, [] as string[]); + let keyList: string[] = keys.reduce((l, key) => { + let listspec = DocListCast(this.resolvedDataDoc[key]); + if (listspec && listspec.length) return [...l, key]; + return l; + }, [] as string[]); keys.map(key => Cast(this.resolvedDataDoc[key], Doc) instanceof Doc && keyList.push(key)); if (LinkManager.Instance.getAllRelatedLinks(this.props.document).length > 0) keyList.push("links"); if (keyList.indexOf(this.fieldKey) !== -1) { keyList.splice(keyList.indexOf(this.fieldKey), 1); } keyList.splice(0, 0, this.fieldKey); - return keyList; + return keyList.filter((item, index) => keyList.indexOf(item) >= index); } /** * Renders the EditableView title element for placement into the tree. @@ -233,7 +236,7 @@ class TreeView extends React.Component<TreeViewProps> { onWorkspaceContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped()) { // 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: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)) }); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, kvp, "onRight"); }, icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) { ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" }); ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" }); @@ -322,14 +325,14 @@ class TreeView extends React.Component<TreeViewProps> { this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)} </ul >; } else { - console.log("PW = " + this.props.panelWidth()); - contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.props.panelHeight() }} key={this.props.document[Id]}> + let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc); + contentElement = <div ref={this._dref} style={{ display: "inline-block", height: layoutDoc[HeightSym]() }} key={this.props.document[Id]}> <CollectionSchemaPreview - Document={this.props.document} + Document={layoutDoc} DataDocument={this.resolvedDataDoc} renderDepth={this.props.renderDepth} width={docWidth} - height={this.props.panelHeight} + height={layoutDoc[HeightSym]} getTransform={this.docTransform} CollectionView={undefined} addDocument={emptyFunction as any} @@ -365,7 +368,7 @@ class TreeView extends React.Component<TreeViewProps> { remove: ((doc: Doc) => boolean), move: DragManager.MoveFunction, dropAction: dropActionType, - addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void, + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void, screenToLocalXf: () => Transform, outerXf: () => { translateX: number, translateY: number }, active: () => boolean, diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 0517c17bf..e500e5c70 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -63,7 +63,7 @@ export class CollectionView extends React.Component<FieldViewProps> { otherdoc.height = 50; Doc.GetProto(otherdoc).title = "applied(" + this.props.Document.title + ")"; Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(this.props.Document); - this.props.addDocTab && this.props.addDocTab(otherdoc, otherdoc, "onRight"); + this.props.addDocTab && this.props.addDocTab(otherdoc, undefined, "onRight"); }), icon: "project-diagram" }); } diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index fa7058d5a..b29a30069 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -9,7 +9,7 @@ import { CollectionDockingView } from "./CollectionDockingView"; import { NumCast } from "../../../new_fields/Types"; import { CollectionViewType } from "./CollectionBaseView"; -type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc, location: string): void }; +type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; @observer export class SelectorContextMenu extends React.Component<SelectorProps> { @observable private _docs: { col: Doc, target: Doc }[] = []; @@ -43,7 +43,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { col.panX = newPanX; col.panY = newPanY; } - this.props.addDocTab(col, col, "inTab"); // bcz: dataDoc? + this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc? }; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 5ac2e1f9c..ccf261c95 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,7 +1,7 @@ @import "../../globalCssVariables"; .collectionfreeformview-ease { - position: absolute; + position: inherit; top: 0; left: 0; width: 100%; @@ -25,8 +25,9 @@ height: 100%; width: 100%; } + >.jsx-parser { - z-index:0; + z-index: 0; } //nested freeform views @@ -40,25 +41,27 @@ border-radius: $border-radius; box-sizing: border-box; position: absolute; + .marqueeView { overflow: hidden; } + top: 0; left: 0; width: 100%; height: 100%; } - + .collectionfreeformview-overlay { .collectionfreeformview>.jsx-parser { position: inherit; height: 100%; } - + >.jsx-parser { - position:absolute; - z-index:0; + position: absolute; + z-index: 0; } .formattedTextBox-cont { @@ -72,9 +75,11 @@ box-sizing: border-box; position:absolute; z-index: -1; + .marqueeView { overflow: hidden; } + top: 0; left: 0; width: 100%; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 996032b1d..b1aba10bf 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -28,6 +28,7 @@ import { MarqueeView } from "./MarqueeView"; import React = require("react"); import v5 = require("uuid/v5"); + export const panZoomSchema = createSchema({ panX: "number", panY: "number", @@ -49,9 +50,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @computed get nativeHeight() { return this.Document.nativeHeight || 0; } public get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private panX = () => this.Document.panX || 0; - private panY = () => this.Document.panY || 0; - private zoomScaling = () => this.Document.scale || 1; + private panX = () => this.props.fitToBox ? this.props.fitToBox[0] : this.Document.panX || 0; + private panY = () => this.props.fitToBox ? this.props.fitToBox[1] : this.Document.panY || 0; + private zoomScaling = () => this.props.fitToBox ? this.props.fitToBox[2] : this.Document.scale || 1; private centeringShiftX = () => !this.nativeWidth ? this._pwidth / 2 : 0; // shift so pan position is at center of window for non-overlay collections private centeringShiftY = () => !this.nativeHeight ? this._pheight / 2 : 0;// shift so pan position is at center of window for non-overlay collections private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); @@ -239,8 +240,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { var scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY)); - this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; - this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY; + // this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; + // this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY; + this.props.Document.panX = panX; + if (this.props.Document.scrollY) { + this.props.Document.scrollY = panY; + this.props.Document.panY = panY; + } + else { + + this.props.Document.panY = panY; + } } @action @@ -326,13 +336,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } - getDocumentViewProps(layoutDoc: Doc): DocumentViewProps { - let datadoc = BoolCast(this.props.Document.isTemplate) || this.props.DataDoc === this.props.Document ? undefined : this.props.DataDoc; - if (Cast(layoutDoc.layout, Doc) instanceof Doc) { // if this document is using a template to render, then set the dataDoc for the template to be this document - datadoc = layoutDoc; - } + getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps { + let resolvedDataDoc = this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; + let layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc); return { - DataDoc: datadoc, + DataDoc: resolvedDataDoc !== layoutDoc && resolvedDataDoc ? resolvedDataDoc : undefined, Document: layoutDoc, addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, @@ -353,6 +361,29 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getScale: this.getScale }; } + getDocumentViewProps(layoutDoc: Doc): DocumentViewProps { + return { + DataDoc: this.props.DataDoc, + Document: this.props.Document, + addDocument: this.props.addDocument, + removeDocument: this.props.removeDocument, + moveDocument: this.props.moveDocument, + ScreenToLocalTransform: this.getTransform, + renderDepth: this.props.renderDepth + 1, + selectOnLoad: layoutDoc[Id] === this._selectOnLoaded, + PanelWidth: layoutDoc[WidthSym], + PanelHeight: layoutDoc[HeightSym], + ContentScaling: returnOne, + ContainingCollectionView: this.props.CollectionView, + focus: this.focusDocument, + parentActive: this.props.active, + whenActiveChanged: this.props.whenActiveChanged, + bringToFront: this.bringToFront, + addDocTab: this.props.addDocTab, + zoomToScale: this.zoomToScale, + getScale: this.getScale + }; + } @computed.struct get views() { @@ -363,7 +394,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (Math.round(page) === Math.round(curPage) || page === -1) { let minim = BoolCast(doc.isMinimized, false); if (minim === undefined || !minim) { - prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />); + prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getChildDocumentViewProps(doc)} />); } } return prev; @@ -407,12 +438,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } private childViews = () => [ - <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} DataDoc={this.props.DataDoc} />, + <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />, ...this.views ] render() { const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`; const easing = () => this.props.Document.panTransformType === "Ease"; + if (this.props.fieldExt) Doc.UpdateDocumentExtensionForField(this.extensionDoc, this.props.fieldKey); return ( <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel} diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index cfbf4aab8..fec105516 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -19,6 +19,7 @@ $serif: "Crimson Text", serif; // misc values $border-radius: 0.3em; // +$search-thumnail-size: 175; // dragged items $contextMenu-zindex: 1000; // context menu shows up over everything @@ -33,4 +34,5 @@ $MAX_ROW_HEIGHT: 44px; COLLECTION_BORDER_WIDTH: $COLLECTION_BORDER_WIDTH; MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE; MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT; + SEARCH_THUMBNAIL_SIZE: $search-thumnail-size; }
\ No newline at end of file diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/globalCssVariables.scss.d.ts index 9788d31f7..d95cec9d8 100644 --- a/src/client/views/globalCssVariables.scss.d.ts +++ b/src/client/views/globalCssVariables.scss.d.ts @@ -4,6 +4,7 @@ interface IGlobalScss { COLLECTION_BORDER_WIDTH: string; MINIMIZED_ICON_SIZE: string; MAX_ROW_HEIGHT: string; + SEARCH_THUMBNAIL_SIZE: string; } declare const globalCssVariables: IGlobalScss; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 4ac43ef4d..9edde896f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -71,6 +71,7 @@ export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; Document: Doc; DataDoc?: Doc; + fitToBox?: number[]; addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean; removeDocument?: (doc: Doc) => boolean; moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; @@ -84,7 +85,7 @@ export interface DocumentViewProps { parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc) => void; - addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void; zoomToScale: (scale: number) => void; getScale: () => number; @@ -254,7 +255,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu DocumentView._undoBatch = UndoManager.StartBatch("iconAnimating"); } let isMinimized: boolean | undefined; - expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => { + expandedDocs.map(maximizedDoc => { let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) { if (isMinimized === undefined) { @@ -321,7 +322,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (dataDocs) { expandedDocs.forEach(maxDoc => (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) && - this.props.addDocTab(getDispDoc(maxDoc), getDispDoc(maxDoc), maxLocation))); + this.props.addDocTab(getDispDoc(maxDoc), undefined, maxLocation))); } } else { let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); @@ -333,7 +334,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let linkedPages = [linkedDocs.length ? NumCast(linkedDocs[0].anchor1Page, undefined) : NumCast(linkedDocs[0].anchor2Page, undefined), linkedDocs.length ? NumCast(linkedDocs[0].anchor2Page, undefined) : NumCast(linkedDocs[0].anchor1Page, undefined)]; let maxLocation = StrCast(linkedDoc.maximizeLocation, "inTab"); - DocumentManager.Instance.jumpToDocument(linkedDoc, ctrlKey, false, document => this.props.addDocTab(document, document, maxLocation), linkedPages[altKey ? 1 : 0]); + DocumentManager.Instance.jumpToDocument(linkedDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, maxLocation), linkedPages[altKey ? 1 : 0]); // else if (linkedToDocs.length || linkedFromDocs.length) { // let linkedFwdDocs = [ @@ -351,7 +352,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // if (!linkedFwdDocs.some(l => l instanceof Promise)) { // let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab"); // let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; - // DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, document => this.props.addDocTab(document, document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext); + // DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, document => this.props.addDocTab(document, undefined, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext); // } } } @@ -393,7 +394,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); }; - fieldsClicked = (): void => { let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, kvp, "onRight"); }; + fieldsClicked = (): void => { let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.dataDoc, "onRight"); }; makeBtnClicked = (): void => { let doc = Doc.GetProto(this.props.Document); doc.isButton = !BoolCast(doc.isButton, false); @@ -406,8 +407,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu doc.nativeWidth = doc.nativeHeight = undefined; } } - fullScreenClicked = (): void => { - CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeAlias(this.props.Document), this.dataDoc); + public fullScreenClicked = (): void => { + CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this); SelectionManager.DeselectAll(); } @@ -503,7 +504,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: "Find aliases", event: async () => { const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document); - this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), Docs.SchemaDocument(["title"], aliases, {}), "onRight"); // bcz: dataDoc? + this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc? }, icon: "search" }); cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 55f61ddff..3f5a2e744 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -18,6 +18,8 @@ import { ImageBox } from "./ImageBox"; import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; import { Id } from "../../../new_fields/FieldSymbols"; +import { BoolCast, Cast } from "../../../new_fields/Types"; +import { DarpaDatasetDoc } from "../../northstar/model/idea/idea"; // @@ -28,6 +30,8 @@ import { Id } from "../../../new_fields/FieldSymbols"; export interface FieldViewProps { fieldKey: string; fieldExt: string; + leaveNativeSize?: boolean; + fitToBox?: number[]; ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; Document: Doc; DataDoc?: Doc; @@ -36,7 +40,7 @@ export interface FieldViewProps { renderDepth: number; selectOnLoad: boolean; addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; - addDocTab: (document: Doc, dataDoc: Doc, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; @@ -72,7 +76,7 @@ export class FieldView extends React.Component<FieldViewProps> { return <FormattedTextBox {...this.props} />; } else if (field instanceof ImageField) { - return <ImageBox {...this.props} />; + return <ImageBox {...this.props} leaveNativeSize={true} />; } else if (field instanceof IconField) { return <IconBox {...this.props} />; @@ -86,7 +90,7 @@ export class FieldView extends React.Component<FieldViewProps> { return <p>{field.date.toLocaleString()}</p>; } else if (field instanceof Doc) { - return <p><b>{field.title + " + " + field[Id]}</b></p>; + return <p><b>{field.title + " : id= " + field[Id]}</b></p>; // let returnHundred = () => 100; // return ( // <DocumentContentsView Document={field} diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 2a45aeb43..07cd43ce3 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -211,7 +211,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; }, - field => this._editorView && !this._applyingChange && + field => this._editorView && !this._applyingChange && this.props.Document[this.props.fieldKey] instanceof RichTextField && this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))) ); this.setupEditor(config, this.dataDoc, this.props.fieldKey); @@ -247,6 +247,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this.props.selectOnLoad) { if (!this.props.isOverlay) this.props.select(false); else this._editorView!.focus(); + this.tryUpdateHeight(); } } @@ -280,7 +281,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; if (this._linkClicked) { DocServer.GetRefField(this._linkClicked).then(f => { - (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, false, document => this.props.addDocTab(document, document, "inTab")); + (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, false, document => this.props.addDocTab(document, undefined, "inTab")); }); e.stopPropagation(); e.preventDefault(); @@ -382,6 +383,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); } + this.tryUpdateHeight(); + } + + @action + tryUpdateHeight() { if (this.props.isOverlay && this.props.Document.autoHeight) { let xf = this._ref.current!.getBoundingClientRect(); let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, xf.height); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 5c00d9d44..06bf65f73 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -20,6 +20,8 @@ import { positionSchema } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); +import { RouteStore } from '../../../server/RouteStore'; +var requestImageSize = require('../../util/request-image-size'); var path = require('path'); @@ -27,7 +29,7 @@ library.add(faImage); export const pageSchema = createSchema({ - curPage: "number" + curPage: "number", }); type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>; @@ -41,7 +43,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD private _downX: number = 0; private _downY: number = 0; private _lastTap: number = 0; - @observable private _photoIndex: number = 0; @observable private _isOpen: boolean = false; private dropDisposer?: DragManager.DragDropDisposer; @@ -115,20 +116,21 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD e.stopPropagation(); } + @action lightbox = (images: string[]) => { if (this._isOpen) { return (<Lightbox - mainSrc={images[this._photoIndex]} - nextSrc={images[(this._photoIndex + 1) % images.length]} - prevSrc={images[(this._photoIndex + images.length - 1) % images.length]} + mainSrc={images[this.Document.curPage || 0]} + nextSrc={images[((this.Document.curPage || 0) + 1) % images.length]} + prevSrc={images[((this.Document.curPage || 0) + images.length - 1) % images.length]} onCloseRequest={action(() => this._isOpen = false )} onMovePrevRequest={action(() => - this._photoIndex = (this._photoIndex + images.length - 1) % images.length + this.Document.curPage = ((this.Document.curPage || 0) + images.length - 1) % images.length )} onMoveNextRequest={action(() => - this._photoIndex = (this._photoIndex + 1) % images.length + this.Document.curPage = ((this.Document.curPage || 0) + 1) % images.length )} />); } @@ -160,7 +162,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD @action onDotDown(index: number) { - this._photoIndex = index; this.Document.curPage = index; } @@ -170,7 +171,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let left = (nativeWidth - paths.length * dist) / 2; return paths.map((p, i) => <div className="imageBox-placer" key={i} > - <div className="imageBox-dot" style={{ background: (i === this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} /> + <div className="imageBox-dot" style={{ background: (i === this.Document.curPage ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} /> </div> ); } @@ -199,6 +200,22 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD } } _curSuffix = "_m"; + + resize(srcpath: string, layoutdoc: Doc) { + requestImageSize(window.origin + RouteStore.corsProxy + "/" + srcpath) + .then((size: any) => { + let aspect = size.height / size.width; + if (Math.abs(layoutdoc[HeightSym]() / layoutdoc[WidthSym]() - aspect) > 0.01) { + setTimeout(action(() => { + layoutdoc.height = layoutdoc[WidthSym]() * aspect; + layoutdoc.nativeHeight = size.height; + layoutdoc.nativeWidth = size.width; + }), 0); + } + }) + .catch((err: any) => console.log(err)); + } + render() { // let transform = this.props.ScreenToLocalTransform().inverse(); let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; @@ -212,6 +229,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let paths: string[] = ["http://www.cs.brown.edu/~bcz/noImage.png"]; // this._curSuffix = ""; // if (w > 20) { + Doc.UpdateDocumentExtensionForField(this.extensionDoc, this.props.fieldKey); let alts = DocListCast(this.extensionDoc.Alternates); let altpaths: string[] = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url)); let field = this.dataDoc[this.props.fieldKey]; @@ -225,8 +243,10 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let rotation = NumCast(this.dataDoc.rotation, 0); let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1; let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; - Doc.UpdateDocumentExtensionForField(this.extensionDoc, this.props.fieldKey); - let srcpath = paths[Math.min(paths.length, this._photoIndex)]; + let srcpath = paths[Math.min(paths.length, this.Document.curPage || 0)]; + + if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document); + return ( <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }} onPointerDown={this.onPointerDown} @@ -235,7 +255,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={srcpath} style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }} - // style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }} + // style={{ objectFit: (this.Document.curPage === 0 ? undefined : "contain") }} width={nativeWidth} ref={this._imgRef} onError={this.onError} /> diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 4beb70284..0e798d291 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -99,8 +99,16 @@ export class KeyValueBox extends React.Component<FieldViewProps> { let rows: JSX.Element[] = []; let i = 0; + const self = this; for (let key of Object.keys(ids).sort()) { - rows.push(<KeyValuePair doc={realDoc} ref={(el) => { if (el) this.rows.push(el); }} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />); + rows.push(<KeyValuePair doc={realDoc} ref={(function () { + let oldEl: KeyValuePair | undefined; + return (el: KeyValuePair) => { + if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1); + oldEl = el; + if (el) self.rows.push(el); + }; + })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />); } return rows; } @@ -145,7 +153,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } getTemplate = async () => { - let parent = Docs.FreeformDocument([], { width: 800, height: 800, title: "Template" }); + let parent = Docs.StackingDocument([], { width: 800, height: 800, title: "Template" }); + parent.singleColumn = false; + parent.columnWidth = 50; for (let row of this.rows.filter(row => row.isChecked)) { await this.createTemplateField(parent, row); row.uncheck(); @@ -167,8 +177,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> { let template = Doc.MakeAlias(target); template.proto = parent; template.title = metaKey; - template.nativeWidth = 300; - template.nativeHeight = 300; + template.nativeWidth = 0; + template.nativeHeight = 0; template.embed = true; template.isTemplate = true; template.templates = new List<string>([Templates.TitleBar(metaKey)]); @@ -189,7 +199,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { if (field instanceof RichTextField || typeof field === "string" || typeof field === "number") { return Docs.TextDocument(options); } else if (field instanceof List) { - return Docs.FreeformDocument([], options); + return Docs.StackingDocument([], options); } else if (field instanceof ImageField) { return Docs.ImageDocument("https://www.freepik.com/free-icon/picture-frame-with-mountain-image_748687.htm", options); } @@ -218,4 +228,4 @@ export class KeyValueBox extends React.Component<FieldViewProps> { {dividerDragger} </div>); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx index 732e6de84..4dee6741f 100644 --- a/src/client/views/nodes/LinkMenuItem.tsx +++ b/src/client/views/nodes/LinkMenuItem.tsx @@ -7,7 +7,7 @@ import { undoBatch } from "../../util/UndoManager"; import './LinkMenu.scss'; import React = require("react"); import { Doc } from '../../../new_fields/Doc'; -import { StrCast, Cast } from '../../../new_fields/Types'; +import { StrCast, Cast, BoolCast, FieldValue } from '../../../new_fields/Types'; import { observable, action } from 'mobx'; import { LinkManager } from '../../util/LinkManager'; import { DragLinkAsDocument } from '../../util/DragManager'; @@ -32,10 +32,15 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { @undoBatch onFollowLink = async (e: React.PointerEvent): Promise<void> => { e.stopPropagation(); - if (DocumentManager.Instance.getDocumentView(this.props.destinationDoc)) { - DocumentManager.Instance.jumpToDocument(this.props.destinationDoc, e.altKey); + let jumpToDoc = this.props.destinationDoc; + let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc)); + if (pdfDoc) { + jumpToDoc = pdfDoc; + } + if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey); } else { - CollectionDockingView.Instance.AddRightSplit(this.props.destinationDoc, undefined); + CollectionDockingView.Instance.AddRightSplit(jumpToDoc, undefined); } } diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 8fb18e8fc..83dedb71d 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -217,10 +217,12 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen @action onScroll = (e: React.UIEvent<HTMLDivElement>) => { + if (e.currentTarget) { this._scrollY = e.currentTarget.scrollTop; let ccv = this.props.ContainingCollectionView; if (ccv) { + ccv.props.Document.panTransformType = "None"; ccv.props.Document.scrollY = this._scrollY; } } diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx new file mode 100644 index 000000000..73fca229b --- /dev/null +++ b/src/client/views/pdf/Annotation.tsx @@ -0,0 +1,144 @@ +import React = require("react"); +import { Doc, DocListCast, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { AnnotationTypes, Viewer, scale } from "./PDFViewer"; +import { observer } from "mobx-react"; +import { observable, IReactionDisposer, reaction, action } from "mobx"; +import { BoolCast, NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import PDFMenu from "./PDFMenu"; +import { DocumentManager } from "../../util/DocumentManager"; + +interface IAnnotationProps { + anno: Doc; + index: number; + parent: Viewer; +} + +export default class Annotation extends React.Component<IAnnotationProps> { + render() { + let annotationDocs = DocListCast(this.props.anno.annotations); + let res = annotationDocs.map(a => { + let type = NumCast(a.type); + switch (type) { + // case AnnotationTypes.Pin: + // return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; + case AnnotationTypes.Region: + return <RegionAnnotation parent={this.props.parent} document={a} index={this.props.index} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; + default: + return <div></div>; + } + }); + return res; + } +} + +interface IRegionAnnotationProps { + x: number; + y: number; + width: number; + height: number; + index: number; + parent: Viewer; + document: Doc; +} + +@observer +class RegionAnnotation extends React.Component<IRegionAnnotationProps> { + @observable private _backgroundColor: string = "red"; + + private _reactionDisposer?: IReactionDisposer; + private _scrollDisposer?: IReactionDisposer; + private _mainCont: React.RefObject<HTMLDivElement>; + + constructor(props: IRegionAnnotationProps) { + super(props); + + this._mainCont = React.createRef(); + } + + componentDidMount() { + this._reactionDisposer = reaction( + () => BoolCast(this.props.document.delete), + () => { + if (BoolCast(this.props.document.delete)) { + if (this._mainCont.current) { + this._mainCont.current.style.display = "none"; + } + } + }, + { fireImmediately: true } + ); + + this._scrollDisposer = reaction( + () => this.props.parent.Index, + () => { + if (this.props.parent.Index === this.props.index) { + this.props.parent.scrollTo(this.props.y - 50); + } + } + ); + } + + componentWillUnmount() { + this._reactionDisposer && this._reactionDisposer(); + this._scrollDisposer && this._scrollDisposer(); + } + + deleteAnnotation = () => { + let annotation = DocListCast(this.props.parent.props.parent.Document.annotations); + let group = FieldValue(Cast(this.props.document.group, Doc)); + if (group && annotation.indexOf(group) !== -1) { + let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc))); + this.props.parent.props.parent.Document.annotations = new List<Doc>(newAnnotations); + } + + if (group) { + let groupAnnotations = DocListCast(group.annotations); + groupAnnotations.forEach(anno => anno.delete = true); + } + + PDFMenu.Instance.fadeOut(true); + } + + @action + onPointerDown = (e: React.PointerEvent) => { + if (e.button === 0) { + let targetDoc = Cast(this.props.document.target, Doc, null); + if (targetDoc) { + DocumentManager.Instance.jumpToDocument(targetDoc, true); + } + } + if (e.button === 2) { + PDFMenu.Instance.Status = "annotation"; + PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this); + PDFMenu.Instance.Pinned = false; + PDFMenu.Instance.AddTag = this.addTag.bind(this); + PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); + } + } + + addTag = (key: string, value: string): boolean => { + let group = FieldValue(Cast(this.props.document.group, Doc)); + if (group) { + let valNum = parseInt(value); + group[key] = isNaN(valNum) ? value : valNum; + return true; + } + return false; + } + + render() { + return ( + <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont} + style={{ + top: this.props.y * scale, + left: this.props.x * scale, + width: this.props.width * scale, + height: this.props.height * scale, + pointerEvents: "all", + backgroundColor: this.props.parent.Index === this.props.index ? "goldenrod" : StrCast(this.props.document.color) + }}></div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 11d3d7e27..0fde764d0 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -41,6 +41,9 @@ } } } +.pdfViewer-viewerCont { + width:100%; +} .page-cont { .textLayer { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index bafc3cbae..bb148e738 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -23,6 +23,7 @@ import { UndoManager } from "../../util/UndoManager"; import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting"; import { ScriptField } from "../../../new_fields/ScriptField"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Annotation from "./Annotation"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); export const scale = 2; @@ -48,7 +49,7 @@ export class PDFViewer extends React.Component<IPDFViewerProps> { render() { return ( - <div ref={this._mainDiv}> + <div className="pdfViewer-viewerCont" ref={this._mainDiv}> {!this._pdf ? (null) : <Viewer pdf={this._pdf} loaded={this.props.loaded} scrollY={this.props.scrollY} parent={this.props.parent} mainCont={this._mainDiv} url={this.props.url} />} </div> @@ -69,7 +70,7 @@ interface IViewerProps { * Handles rendering and virtualization of the pdf */ @observer -class Viewer extends React.Component<IViewerProps> { +export class Viewer extends React.Component<IViewerProps> { // _visibleElements is the array of JSX elements that gets rendered @observable.shallow private _visibleElements: JSX.Element[] = []; // _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder @@ -192,10 +193,14 @@ class Viewer extends React.Component<IViewerProps> { let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages); this._isPage = Array<string>(this.props.pdf.numPages); // this._textContent = Array<Pdfjs.TextContent>(this.props.pdf.numPages); + const proms: Pdfjs.PDFPromise<any>[] = []; for (let i = 0; i < this.props.pdf.numPages; i++) { - await this.props.pdf.getPage(i + 1).then(page => runInAction(() => { - // pageSizes[i] = { width: page.view[2] * scale, height: page.view[3] * scale }; - let x = page.getViewport(scale); + proms.push(this.props.pdf.getPage(i + 1).then(page => runInAction(() => { + pageSizes[i] = { + width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale, + height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale + }; + // let x = page.getViewport(scale); // page.getTextContent().then((text: Pdfjs.TextContent) => { // // let tc = new Pdfjs.TextContentItem() // // let tc = {str: } @@ -204,9 +209,10 @@ class Viewer extends React.Component<IViewerProps> { // // tcStr += t.str; // // }) // }); - pageSizes[i] = { width: x.width, height: x.height }; - })); + // pageSizes[i] = { width: x.width, height: x.height }; + }))); } + await Promise.all(proms); runInAction(() => Array.from(Array((this._pageSizes = pageSizes).length).keys()).map(this.getPlaceholderPage)); this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); @@ -226,7 +232,8 @@ class Viewer extends React.Component<IViewerProps> { let annoDocs: Doc[] = []; let mainAnnoDoc = Docs.CreateInstance(new Doc(), "", {}); - mainAnnoDoc.page = Math.round(Math.random()); + mainAnnoDoc.title = "Annotation on " + StrCast(this.props.parent.Document.title); + mainAnnoDoc.pdfDoc = this.props.parent.Document; this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => { for (let anno of value) { let annoDoc = new Doc(); @@ -244,6 +251,7 @@ class Viewer extends React.Component<IViewerProps> { } }); + mainAnnoDoc.y = Math.max((NumCast(annoDocs[0].y) * scale) - 100, 0); mainAnnoDoc.annotations = new List<Doc>(annoDocs); if (sourceDoc) { DocUtils.MakeLink(sourceDoc, mainAnnoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title)); @@ -417,20 +425,8 @@ class Viewer extends React.Component<IViewerProps> { } } - renderAnnotation = (anno: Doc, index: number): JSX.Element[] => { - let annotationDocs = DocListCast(anno.annotations); - let res = annotationDocs.map(a => { - let type = NumCast(a.type); - switch (type) { - // case AnnotationTypes.Pin: - // return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; - case AnnotationTypes.Region: - return <RegionAnnotation parent={this} document={a} index={index} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; - default: - return <div></div>; - } - }); - return res; + renderAnnotation = (anno: Doc, index: number): JSX.Element => { + return <Annotation anno={anno} index={index} parent={this} />; } @action @@ -676,116 +672,6 @@ export enum AnnotationTypes { Region } -interface IAnnotationProps { - x: number; - y: number; - width: number; - height: number; - index: number; - parent: Viewer; - document: Doc; -} - -@observer -class RegionAnnotation extends React.Component<IAnnotationProps> { - @observable private _backgroundColor: string = "red"; - - private _reactionDisposer?: IReactionDisposer; - private _scrollDisposer?: IReactionDisposer; - private _mainCont: React.RefObject<HTMLDivElement>; - - constructor(props: IAnnotationProps) { - super(props); - - this._mainCont = React.createRef(); - } - - componentDidMount() { - this._reactionDisposer = reaction( - () => BoolCast(this.props.document.delete), - () => { - if (BoolCast(this.props.document.delete)) { - if (this._mainCont.current) { - this._mainCont.current.style.display = "none"; - } - } - }, - { fireImmediately: true } - ); - - this._scrollDisposer = reaction( - () => this.props.parent.Index, - () => { - if (this.props.parent.Index === this.props.index) { - this.props.parent.scrollTo(this.props.y - 50); - } - } - ); - } - - componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); - this._scrollDisposer && this._scrollDisposer(); - } - - deleteAnnotation = () => { - let annotation = DocListCast(this.props.parent.props.parent.Document.annotations); - let group = FieldValue(Cast(this.props.document.group, Doc)); - if (group && annotation.indexOf(group) !== -1) { - let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc))); - this.props.parent.props.parent.Document.annotations = new List<Doc>(newAnnotations); - } - - if (group) { - let groupAnnotations = DocListCast(group.annotations); - groupAnnotations.forEach(anno => anno.delete = true); - } - - PDFMenu.Instance.fadeOut(true); - } - - @action - onPointerDown = (e: React.PointerEvent) => { - if (e.button === 0) { - let targetDoc = Cast(this.props.document.target, Doc, null); - if (targetDoc) { - DocumentManager.Instance.jumpToDocument(targetDoc, true); - } - } - if (e.button === 2) { - PDFMenu.Instance.Status = "annotation"; - PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this); - PDFMenu.Instance.Pinned = false; - PDFMenu.Instance.AddTag = this.addTag.bind(this); - PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); - } - } - - addTag = (key: string, value: string): boolean => { - let group = FieldValue(Cast(this.props.document.group, Doc)); - if (group) { - let valNum = parseInt(value); - group[key] = isNaN(valNum) ? value : valNum; - return true; - } - return false; - } - - render() { - return ( - <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont} - style={{ - top: this.props.y * scale, - left: this.props.x * scale, - width: this.props.width * scale, - height: this.props.height * scale, - pointerEvents: "all", - backgroundColor: this.props.parent.Index === this.props.index ? "goldenrod" : StrCast(this.props.document.color) - }}></div> - ); - } -} - class SimpleLinkService { externalLinkTarget: any = null; externalLinkRel: any = null; diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx index 01aa96705..57e36be43 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -31,7 +31,7 @@ interface IPageProps { makePin: (x: number, y: number, page: number) => void; sendAnnotations: (annotations: HTMLDivElement[], page: number) => void; createAnnotation: (div: HTMLDivElement, page: number) => void; - makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string) => Doc; + makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string, linkTo: boolean) => Doc; getScrollFromPage: (page: number) => number; } @@ -137,7 +137,7 @@ export default class Page extends React.Component<IPageProps> { @action highlight = (targetDoc?: Doc, color: string = "red") => { // creates annotation documents for current highlights - let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color); + let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color, false); let targetAnnotations = Cast(this.props.parent.Document.annotations, listSpec(Doc)); if (targetAnnotations === undefined) { Doc.GetProto(this.props.parent.Document).annotations = new List([annotationDoc]); diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 4afc0210f..d63c0b066 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -13,12 +13,11 @@ import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons'; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; - library.add(faArrowUp); library.add(fileSolid); library.add(faLocationArrow); +library.add(fileRegular as any); library.add(faSearch); -library.add(fileRegular); interface PresentationElementProps { mainDocument: Doc; diff --git a/src/client/views/search/FilterBox.scss b/src/client/views/search/FilterBox.scss index 1eb8963d7..1b73916c9 100644 --- a/src/client/views/search/FilterBox.scss +++ b/src/client/views/search/FilterBox.scss @@ -105,4 +105,60 @@ 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; + } }
\ No newline at end of file diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index cc1feeaf7..6053db595 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -2,8 +2,8 @@ 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 { library} from '@fortawesome/fontawesome-svg-core'; +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 { DocTypes } from '../../documents/Documents'; @@ -19,8 +19,11 @@ 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,12 +38,16 @@ export class FilterBox extends React.Component { public _allIcons: string[] = [DocTypes.AUDIO, DocTypes.COL, DocTypes.HIST, DocTypes.IMG, DocTypes.LINK, DocTypes.PDF, DocTypes.TEXT, DocTypes.VID, DocTypes.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 _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 private _collectionStatus = false; @observable private _collectionSelfStatus = true; @observable private _collectionParentStatus = true; @@ -57,7 +64,7 @@ export class FilterBox extends React.Component { componentDidMount = () => { document.addEventListener("pointerdown", (e) => { - if (e.timeStamp !== this._pointerTime) { + if (!e.defaultPrevented && e.timeStamp !== this._pointerTime) { SearchBox.Instance.closeSearch(); } }); @@ -65,9 +72,9 @@ export class FilterBox extends React.Component { setupAccordion() { $('document').ready(function () { - var acc = document.getElementsByClassName('filter-header'); - - for (var i = 0; i < acc.length; i++) { + const acc = document.getElementsByClassName('filter-header'); + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < acc.length; i++) { acc[i].addEventListener("click", function (this: HTMLElement) { this.classList.toggle("active"); @@ -96,6 +103,7 @@ export class FilterBox extends React.Component { $('document').ready(function () { var acc = document.getElementsByClassName('filter-header'); + // tslint:disable-next-line: prefer-for-of for (var i = 0; i < acc.length; i++) { let classList = acc[i].classList; if (classList.contains("active")) { @@ -240,14 +248,48 @@ export class FilterBox extends React.Component { filterDocsByType(docs: Doc[]) { let finalDocs: Doc[] = []; docs.forEach(doc => { - let layoutresult = Cast(doc.type, "string", ""); - if (this._icons.includes(layoutresult)) { + let layoutresult = Cast(doc.type, "string"); + if (!layoutresult || this._icons.includes(layoutresult)) { finalDocs.push(doc); } }); 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; @@ -259,7 +301,7 @@ export class FilterBox extends React.Component { @action.bound handleWordQueryChange = () => { this._basicWordStatus = !this._basicWordStatus; } - @action + @action.bound getBasicWordStatus() { return this._basicWordStatus; } @action.bound @@ -315,6 +357,31 @@ export class FilterBox extends React.Component { getAuthorStatus() { return this._authorFieldStatus; } getDataStatus() { return this._dataFieldStatus; } + 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 @@ -323,6 +390,7 @@ 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" }}> @@ -376,7 +444,8 @@ export class FilterBox extends React.Component { <button className="reset-filter" onClick={this.resetFilters}>Reset Filters</button> </div> </div> - ) : undefined} + ) : + undefined} </div> ); } diff --git a/src/client/views/search/Pager.tsx b/src/client/views/search/Pager.tsx index 1c62773b1..9bcfb1676 100644 --- a/src/client/views/search/Pager.tsx +++ b/src/client/views/search/Pager.tsx @@ -57,17 +57,17 @@ export class Pager extends React.Component { return ( <div className="search-pager"> <div className="search-arrows"> - <div className="arrow" - onPointerDown={this.onLeftClick} style={SearchBox.Instance._pageNum === 0 ? { opacity: .2 } : this._leftHover ? { opacity: 1 } : { opacity: .7 }} - onMouseEnter={this.mouseInLeft} onMouseOut={this.mouseOutLeft}> + <div className = "arrow" + onPointerDown = {this.onLeftClick} style = {SearchBox.Instance._pageNum === 0 ? {opacity: .2} : this._leftHover ? {opacity: 1} : {opacity: .7}} + onMouseEnter = {this.mouseInLeft} onMouseLeave = {this.mouseOutLeft}> <FontAwesomeIcon className="fontawesome-icon" icon={faArrowCircleLeft} /> </div> <div className="pager-title"> page {SearchBox.Instance._pageNum + 1} of {SearchBox.Instance._maxNum} </div> - <div className="arrow" - onPointerDown={this.onRightClick} style={SearchBox.Instance._pageNum === SearchBox.Instance._maxNum - 1 ? { opacity: .2 } : this._rightHover ? { opacity: 1 } : { opacity: .7 }} - onMouseEnter={this.mouseInRight} onMouseOut={this.mouseOutRight}> + <div className = "arrow" + onPointerDown = {this.onRightClick} style = {SearchBox.Instance._pageNum === SearchBox.Instance._maxNum-1 ? {opacity: .2} : this._rightHover ? {opacity: 1} : {opacity: .7}} + onMouseEnter = {this.mouseInRight} onMouseLeave = {this.mouseOutRight}> <FontAwesomeIcon className="fontawesome-icon" icon={faArrowCircleRight} /> </div> </div> diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 2a27bbe62..7541b328a 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -46,9 +46,10 @@ display: flex; flex-direction: column; margin-right: 72px; - height: 560px; - overflow: hidden; - overflow-y: auto; + // height: 560px; + height: 100%; + // overflow: hidden; + // overflow-y: auto; .no-result { width: 500px; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index f53a4e34f..dc21e4a3c 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -15,7 +15,6 @@ import { Id } from '../../../new_fields/FieldSymbols'; import { SearchUtil } from '../../util/SearchUtil'; import { RouteStore } from '../../../server/RouteStore'; import { FilterBox } from './FilterBox'; -import { Pager } from './Pager'; @observer export class SearchBox extends React.Component { @@ -169,7 +168,6 @@ export class SearchBox extends React.Component { @action.bound closeSearch = () => { - console.log("closing search"); FilterBox.Instance.closeFilter(); this.closeResults(); } @@ -198,9 +196,9 @@ export class SearchBox extends React.Component { ) : this._openNoResults ? (<div className="no-result">No Search Results</div>) : null} </div> - {/* <div style={this._results.length !== 0 ? { display: "flex" } : { display: "none" }}> - <Pager /> - </div> */} + <div style={this._results.length !== 0 ? { display: "flex" } : { display: "none" }}> + {/* <Pager /> */} + </div> </div> ); } diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss index 946680f0e..fa4c9cb38 100644 --- a/src/client/views/search/SearchItem.scss +++ b/src/client/views/search/SearchItem.scss @@ -5,6 +5,7 @@ flex-direction: row-reverse; justify-content: flex-end; height: 70px; + z-index: 0; .search-item { width: 500px; @@ -13,6 +14,7 @@ border-bottom-style: solid; padding: 10px; height: 70px; + z-index: 0; display: inline-block; .main-search-info { @@ -23,16 +25,17 @@ .search-title { text-transform: uppercase; text-align: left; - width: 80%; + width: 100%; font-weight: bold; } .search-info { display: flex; justify-content: flex-end; - width: 40%; .link-container.item { + margin-left: auto; + margin-right: auto; height: 26px; width: 26px; border-radius: 13px; @@ -41,7 +44,6 @@ display: flex; justify-content: center; align-items: center; - right: 15px; -webkit-transition: all 0.2s ease-in-out; -moz-transition: all 0.2s ease-in-out; -o-transition: all 0.2s ease-in-out; @@ -84,26 +86,42 @@ .link-container.item:hover .link-extended { opacity: 1; } + + .icon-icons { + width:50px + } + .icon-live { + width:175px; + } - .icon { - + .icon-icons, .icon-live { + height:50px; + margin:auto; + overflow: hidden; .search-type { - width: 25PX; - height: 25PX; - display: flex; + display: inline-block; + width:100%; + position: absolute; justify-content: center; align-items: center; position: relative; margin-right: 5px; } + .pdfBox-cont { + overflow: hidden; + + img { + width:100% !important; + height:auto !important; + } + } .search-type:hover+.search-label { opacity: 1; } .search-label { font-size: 10; - padding: 5px; position: relative; right: 0px; text-transform: capitalize; @@ -114,6 +132,18 @@ transition: opacity 0.2s ease-in-out; } } + + .icon-live:hover { + height:175px; + .pdfBox-cont { + img { + width:100% !important; + } + } + } + } + .search-info:hover { + width:60%; } } } @@ -143,7 +173,15 @@ -webkit-transform: scale(0); -ms-transform: scale(0); transform: scale(0); - // height: 100% } +} +.search-overview:hover { + z-index: 1; +} +.collection { + display:flex; +} +.collection-item { + width:35px; }
\ No newline at end of file diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 5160d9469..f4ea3ee09 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -1,24 +1,26 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia } from '@fortawesome/free-solid-svg-icons'; +import { faCaretUp, faChartBar, faFilePdf, faFilm, faGlobeAsia, faImage, faLink, faMusic, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Cast, NumCast } from "../../../new_fields/Types"; -import { observable, runInAction, computed, action } from "mobx"; -import { listSpec } from "../../../new_fields/Schema"; -import { Doc } from "../../../new_fields/Doc"; +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { emptyFunction, returnFalse, returnOne } from "../../../Utils"; +import { DocTypes } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { SetupDrag } from "../../util/DragManager"; +import { LinkManager } from "../../util/LinkManager"; import { SearchUtil } from "../../util/SearchUtil"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { observer } from "mobx-react"; -import "./SearchItem.scss"; +import { Transform } from "../../util/Transform"; +import { SEARCH_THUMBNAIL_SIZE } from "../../views/globalCssVariables.scss"; import { CollectionViewType } from "../collections/CollectionBaseView"; -import { DocTypes } from "../../documents/Documents"; -import { FilterBox } from "./FilterBox"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; import { DocumentView } from "../nodes/DocumentView"; -import "./SelectorContextMenu.scss"; import { SearchBox } from "./SearchBox"; +import "./SearchItem.scss"; +import "./SelectorContextMenu.scss"; export interface SearchItemProps { doc: Doc; @@ -69,18 +71,63 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> { CollectionDockingView.Instance.AddRightSplit(col, undefined); }; } - render() { return ( - < div className="parents"> + <div className="parents"> <p className="contexts">Contexts:</p> - {this._docs.map(doc => <div className="collection"><a className="title" onClick={this.getOnClick(doc)}>{doc.col.title}</a></div>)} - {this._otherDocs.map(doc => <div className="collection"><a className="title" onClick={this.getOnClick(doc)}>{doc.col.title}</a></div>)} + {[...this._docs, ...this._otherDocs].map(doc => { + let item = React.createRef<HTMLDivElement>(); + return <div className="collection" key={doc.col[Id] + doc.target[Id]} ref={item}> + <div className="collection-item" onPointerDown={ + SetupDrag(item, () => doc.col, undefined, undefined, undefined, undefined, () => SearchBox.Instance.closeSearch())}> + <FontAwesomeIcon icon={faStickyNote} /> + </div> + <a onClick={this.getOnClick(doc)}>{doc.col.title}</a> + </div>; + })} </div> ); } } +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> { @@ -89,10 +136,56 @@ export class SearchItem extends React.Component<SearchItemProps> { onClick = () => { DocumentManager.Instance.jumpToDocument(this.props.doc, false); } + @observable _useIcons = true; + @observable _displayDim = 50; @computed public get DocumentIcon() { - let layoutresult = Cast(this.props.doc.type, "string", ""); + let layoutresult = StrCast(this.props.doc.type); + if (!this._useIcons) { + let renderDoc = this.props.doc; + let box: number[] = []; + if (layoutresult.indexOf(DocTypes.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 }); + 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(renderDoc.nativeWidth, returnXDimension()); + return <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 + fitToBox={box} + Document={renderDoc} + addDocument={returnFalse} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + addDocTab={returnFalse} + renderDepth={1} + PanelWidth={returnXDimension} + PanelHeight={returnYDimension} + focus={emptyFunction} + selectOnLoad={false} + parentActive={returnFalse} + whenActiveChanged={returnFalse} + bringToFront={emptyFunction} + zoomToScale={emptyFunction} + getScale={returnOne} + ContainingCollectionView={undefined} + ContentScaling={scale} + /> + </div>; + } let button = layoutresult.indexOf(DocTypes.PDF) !== -1 ? faFilePdf : layoutresult.indexOf(DocTypes.IMG) !== -1 ? faImage : @@ -104,7 +197,9 @@ export class SearchItem extends React.Component<SearchItemProps> { layoutresult.indexOf(DocTypes.HIST) !== -1 ? faChartBar : layoutresult.indexOf(DocTypes.WEB) !== -1 ? faGlobeAsia : faCaretUp; - return <FontAwesomeIcon icon={button} size="2x" />; + return <div onPointerDown={action(() => { this._useIcons = false; this._displayDim = Number(SEARCH_THUMBNAIL_SIZE); })} > + <FontAwesomeIcon icon={button} size="2x" /> + </div>; } collectionRef = React.createRef<HTMLDivElement>(); @@ -119,7 +214,7 @@ export class SearchItem extends React.Component<SearchItemProps> { } @computed - get linkCount() { return Cast(this.props.doc.linkedToDocs, listSpec(Doc), []).length + Cast(this.props.doc.linkedFromDocs, listSpec(Doc), []).length; } + get linkCount() { return LinkManager.Instance.getAllRelatedLinks(this.props.doc).length; } @computed get linkString(): string { @@ -130,45 +225,65 @@ export class SearchItem extends React.Component<SearchItemProps> { return num.toString() + " links"; } - pointerDown = (e: React.PointerEvent) => { SearchBox.Instance.openSearch(e); }; + @action + pointerDown = (e: React.PointerEvent) => SearchBox.Instance.openSearch(e) highlightDoc = (e: React.PointerEvent) => { - let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc); - docViews.forEach(element => { - element.props.Document.libraryBrush = true; - }); + if (this.props.doc.type === DocTypes.LINK) { + if (this.props.doc.anchor1 && this.props.doc.anchor2) { + + let doc1 = Cast(this.props.doc.anchor1, Doc, new Doc()); + let doc2 = Cast(this.props.doc.anchor2, Doc, new Doc()); + doc1.libraryBrush = true; + doc2.libraryBrush = true; + } + } else { + let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc); + docViews.forEach(element => { + element.props.Document.libraryBrush = true; + }); + } } unHighlightDoc = (e: React.PointerEvent) => { - let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc); - docViews.forEach(element => { - element.props.Document.libraryBrush = false; - }); + if (this.props.doc.type === DocTypes.LINK) { + if (this.props.doc.anchor1 && this.props.doc.anchor2) { + + let doc1 = Cast(this.props.doc.anchor1, Doc, new Doc()); + let doc2 = Cast(this.props.doc.anchor2, Doc, new Doc()); + doc1.libraryBrush = false; + doc2.libraryBrush = false; + } + } else { + let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc); + docViews.forEach(element => { + element.props.Document.libraryBrush = false; + }); + } } render() { return ( <div className="search-overview" onPointerDown={this.pointerDown}> - <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} ref={this.collectionRef} id="result" onClick={this.onClick} onPointerDown={() => { - this.pointerDown; - SetupDrag(this.collectionRef, this.startDocDrag); - }} > + <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} ref={this.collectionRef} id="result" + onClick={this.onClick} onPointerDown={this.pointerDown} > <div className="main-search-info"> <div className="search-title" id="result" >{this.props.doc.title}</div> - <div className="search-info"> + <div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}> + <div className={`icon-${this._useIcons ? "icons" : "live"}`}> + <div className="search-type" >{this.DocumentIcon}</div> + <div className="search-label">{this.props.doc.type}</div> + </div> <div className="link-container item"> <div className="link-count">{this.linkCount}</div> <div className="link-extended">{this.linkString}</div> </div> - <div className="icon"> - <div className="search-type" >{this.DocumentIcon}</div> - <div className="search-label">{this.props.doc.type}</div> - </div> </div> </div> </div> <div className="searchBox-instances"> - <SelectorContextMenu {...this.props} /> + {this.props.doc.type === DocTypes.LINK ? <LinkContextMenu doc1 = {Cast(this.props.doc.anchor1, Doc, new Doc())} doc2 = {Cast(this.props.doc.anchor2, Doc, new Doc())}/> : + <SelectorContextMenu {...this.props} /> } </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.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/new_fields/Doc.ts b/src/new_fields/Doc.ts index b0184dd4e..734a90731 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -277,7 +277,29 @@ export namespace Doc { if (!GetT(doc, "isPrototype", "boolean", true)) { return Doc.MakeCopy(doc); } - return new Doc; + return Doc.MakeDelegate(doc); // bcz? + } + + export function expandTemplateLayout(templateLayoutDoc: Doc, dataDoc?: Doc) { + let resolvedDataDoc = (templateLayoutDoc !== dataDoc) ? dataDoc : undefined; + if (!dataDoc || !(templateLayoutDoc && !(Cast(templateLayoutDoc.layout, Doc) instanceof Doc) && resolvedDataDoc && resolvedDataDoc !== templateLayoutDoc)) { + return templateLayoutDoc; + } + // if we have a data doc that doesn't match the layout, then we're rendering a template. + // ... which means we change the layout to be an expanded view of the template layout. + // This allows the view override the template's properties and be referenceable as its own document. + + let expandedTemplateLayout = templateLayoutDoc["_expanded_" + dataDoc[Id]]; + if (expandedTemplateLayout instanceof Doc) { + return expandedTemplateLayout; + } + if (expandedTemplateLayout === undefined) { + setTimeout(() => { + templateLayoutDoc["_expanded_" + dataDoc[Id]] = Doc.MakeDelegate(templateLayoutDoc); + (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).title = templateLayoutDoc.title + " applied to " + dataDoc.title; + }, 0); + } + return templateLayoutDoc; } export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc { diff --git a/src/scraping/acm/chromedriver b/src/scraping/acm/chromedriver Binary files differnew file mode 100755 index 000000000..9e9b16717 --- /dev/null +++ b/src/scraping/acm/chromedriver diff --git a/src/scraping/acm/chromedriver.exe b/src/scraping/acm/chromedriver.exe Binary files differnew file mode 100644 index 000000000..6a362fd43 --- /dev/null +++ b/src/scraping/acm/chromedriver.exe diff --git a/src/scraping/acm/citations.txt b/src/scraping/acm/citations.txt new file mode 100644 index 000000000..e5018ddef --- /dev/null +++ b/src/scraping/acm/citations.txt @@ -0,0 +1,2 @@ +321046 +2412979
\ No newline at end of file diff --git a/src/scraping/acm/index.js b/src/scraping/acm/index.js new file mode 100644 index 000000000..ff4b099e7 --- /dev/null +++ b/src/scraping/acm/index.js @@ -0,0 +1,187 @@ +const { + Builder, + By +} = require('selenium-webdriver'); +const { + readFile, + writeFile +} = require('fs'); + +const driver_pause = 500; // milliseconds +const sample_line_char_max = 100; // characters +const target_browser = 'chrome'; + +// GENERAL UTILITY FUNCTIONS + +function log_read(content) { + process.stdout.write("reading " + content + "..."); +} + +function log_snippet(result, quotes = true) { + let snippet = "failed to create snippet"; + switch (typeof result) { + case "string": + let ellipse = result.length > sample_line_char_max; + let i = sample_line_char_max; + if (ellipse) { + while (result[i] != " " && i < -1) { + i--; + } + } + snippet = `${result.substring(0, i + 1).trim()}${ellipse ? "..." : ""}`; + snippet = quotes ? `"${snippet}"` : snippet; + break; + case "object": + snippet = result.map(res => { + switch (typeof res) { + case "string": + return res.substring(0, sample_line_char_max / result.length); + case "object": + return res[Object.keys(res)[0]]; + } + }).join(', '); + } + console.log(snippet); + return result; +} + +// DRIVER UTILITY FUNCTIONS + +async function navigate_to(url) { + await driver.get(url); + await driver.sleep(driver_pause); +} + +async function click_on(ref) { + await (await locate(ref)).click(); + await driver.sleep(driver_pause); +} + +async function locate(ref, multiple = false) { + let locator = ref.startsWith("//") ? By.xpath(ref) : By.id(ref); + return await multiple ? driver.findElements(locator) : driver.findElement(locator); +} + +async function text_of(ref) { + let element = await locate(ref); + return await element.getText(); +} + +async function text_of_all(ref) { + let elements = await locate(ref, true); + let results = []; + for (let element of elements) { + results.push(await element.getText()); + } + return results; +} + +async function logged_assign(key, value) { + log_read(key); + result[key] = log_snippet(value); +} + +// TEXT SCRAPING + +async function read_authors() { + await click_on('//*[@id="tab-1014-btnInnerEl"]/span'); + + let authors = await text_of('//*[@id="tabpanel-1009-body"]'); + let sanitize = line => line.length > 0 && !(line.startsWith("No contact information") || line.startsWith("View colleagues of") || line.startsWith("Bibliometrics:")); + let author_lines = authors.split("\n").map(line => line.trim()).filter(sanitize); + + let all_authors = []; + let i = 0; + while (i < author_lines.length) { + let individual = []; + while (!author_lines[i].startsWith("Average citations")) { + individual.push(author_lines[i]); + i++; + } + individual.push(author_lines[i]); + all_authors.push(individual); + i++; + } + + return all_authors; +} + +// JSON / DASH CONVERSION AND EXPORT + +function parse_authors(metadata) { + let publicationYears = metadata[1].substring(18).split("-"); + author = { + name: metadata[0], + publication_start: parseInt(publicationYears[0]), + publication_end: parseInt(publicationYears[1]) + }; + for (let count = 2; count < metadata.length; count++) { + let attr = metadata[count]; + let char = attr.length - 1; + while (attr[char] != " ") { + char--; + } + let key = attr.substring(0, char).toLowerCase().replace(/ /g, "_").replace(/[\(\)]/g, ""); + let value = parseFloat(attr.substring(char + 1).replace(/,/g, "")); + author[key] = value; + } + return author; +} + +function write_results() { + console.log(); + let output = ""; + results.forEach(res => output += (JSON.stringify(res, null, 4) + "\n")); + + writeFile("./results.txt", output, function errorHandler(exception) { + console.log(exception || "scraped references successfully written as JSON to ./results.txt\n"); + }); +} + +async function scrape_targets(error, data) { + if (error) { + console.log("\nUnable to collect target citations from a citations.txt file stored in this directory.\nPlease make sure one is provided."); + return; + } + + let references = data.split("\n").map(entry => entry.replace("\r", "")).filter(line => line.match(/\d+/g)); + let quota = references.length; + log_snippet(`found ${quota} references to scrape`, false); + + driver = await new Builder().forBrowser(target_browser).build(); + + for (let i = 0; i < quota; i++) { + try { + result = {}; + + let id = references[i]; + let url = `https://dl.acm.org/citation.cfm?id=${id}`; + console.log(`\nscraping ${i + 1}/${quota} (${id})`); + await navigate_to(url); + + logged_assign("url", url); + logged_assign("title", await text_of('//*[@id="divmain"]/div/h1')); + logged_assign("abstract", (await text_of_all("abstract-body")).join(" ")); + logged_assign("authors", (await read_authors()).map(parse_authors)); + } catch (e) { + console.log(e); + await driver.quit(); + } + + results.push(result); + } + + write_results(); + + await driver.quit(); +} + +let driver; +let results = []; +let result = {}; + +log_read("target references"); + +readFile("./citations.txt", { + encoding: "utf8" +}, scrape_targets);
\ No newline at end of file diff --git a/src/scraping/acm/package.json b/src/scraping/acm/package.json new file mode 100644 index 000000000..10f4d2156 --- /dev/null +++ b/src/scraping/acm/package.json @@ -0,0 +1,17 @@ +{ + "name": "scraper", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^0.19.0", + "cheerio": "^1.0.0-rc.3", + "selenium-webdriver": "^4.0.0-alpha.4" + } +} diff --git a/src/scraping/acm/results.txt b/src/scraping/acm/results.txt new file mode 100644 index 000000000..fffa7ff51 --- /dev/null +++ b/src/scraping/acm/results.txt @@ -0,0 +1,66 @@ +{ + "url": "https://dl.acm.org/citation.cfm?id=321046", + "title": "Integer Programming Formulation of Traveling Salesman Problems", + "abstract": "It has been observed by many people that a striking number of quite diverse mathematical problems can be formulated as problems in integer programming, that is, linear programming problems in which some or all of the variables are required to assume integral values. This fact is rendered quite interesting by recent research on such problems, notably by R. E. Gomory [2, 3], which gives promise of yielding efficient computational techniques for their solution. The present paper provides yet another example of the versatility of integer programming as a mathematical modeling device by representing a generalization of the well-known “Travelling Salesman Problem” in integer programming terms. The authors have developed several such models, of which the one presented here is the most efficient in terms of generality, number of variables, and number of constraints. This model is due to the second author [4] and was presented briefly at the Symposium on Combinatorial Problems held at Princeton University, April 1960, sponsored by SIAM and IBM. The problem treated is: (1) A salesman is required to visit each of n cities, indexed by 1, … , n. He leaves from a “base city” indexed by 0, visits each of the n other cities exactly once, and returns to city 0. During his travels he must return to 0 exactly t times, including his final return (here t may be allowed to vary), and he must visit no more than p cities in one tour. (By a tour we mean a succession of visits to cities without stopping at city 0.) It is required to find such an itinerary which minimizes the total distance traveled by the salesman. Note that if t is fixed, then for the problem to have a solution we must have tp ≧ n. For t = 1, p ≧ n, we have the standard traveling salesman problem. Let dij (i ≠ j = 0, 1, … , n) be the distance covered in traveling from city i to city j. The following integer programming problem will be shown to be equivalent to (1): (2) Minimize the linear form ∑0≦i≠j≦n∑ dijxij over the set determined by the relations ∑ni=0i≠j xij = 1 (j = 1, … , n) ∑nj=0j≠i xij = 1 (i = 1, … , n) ui - uj + pxij ≦ p - 1 (1 ≦ i ≠ j ≦ n) where the xij are non-negative integers and the ui (i = 1, …, n) are arbitrary real numbers. (We shall see that it is permissible to restrict the ui to be non-negative integers as well.) If t is fixed it is necessary to add the additional relation: ∑nu=1 xi0 = t Note that the constraints require that xij = 0 or 1, so that a natural correspondence between these two problems exists if the xij are interpreted as follows: The salesman proceeds from city i to city j if and only if xij = 1. Under this correspondence the form to be minimized in (2) is the total distance to be traveled by the salesman in (1), so the burden of proof is to show that the two feasible sets correspond; i.e., a feasible solution to (2) has xij which do define a legitimate itinerary in (1), and, conversely a legitimate itinerary in (1) defines xij, which, together with appropriate ui, satisfy the constraints of (2). Consider a feasible solution to (2). The number of returns to city 0 is given by ∑ni=1 xi0. The constraints of the form ∑ xij = 1, all xij non-negative integers, represent the conditions that each city (other than zero) is visited exactly once. The ui play a role similar to node potentials in a network and the inequalities involving them serve to eliminate tours that do not begin and end at city 0 and tours that visit more than p cities. Consider any xr0r1 = 1 (r1 ≠ 0). There exists a unique r2 such that xr1r2 = 1. Unless r2 = 0, there is a unique r3 with xr2r3 = 1. We proceed in this fashion until some rj = 0. This must happen since the alternative is that at some point we reach an rk = rj, j + 1 < k. Since none of the r's are zero we have uri - uri + 1 + pxriri + 1 ≦ p - 1 or uri - uri + 1 ≦ - 1. Summing from i = j to k - 1, we have urj - urk = 0 ≦ j + 1 - k, which is a contradiction. Thus all tours include city 0. It remains to observe that no tours is of length greater than p. Suppose such a tour exists, x0r1 , xr1r2 , … , xrprp+1 = 1 with all ri ≠ 0. Then, as before, ur1 - urp+1 ≦ - p or urp+1 - ur1 ≧ p. But we have urp+1 - ur1 + pxrp+1r1 ≦ p - 1 or urp+1 - ur1 ≦ p (1 - xrp+1r1) - 1 ≦ p - 1, which is a contradiction. Conversely, if the xij correspond to a legitimate itinerary, it is clear that the ui can be adjusted so that ui = j if city i is the jth city visited in the tour which includes city i, for we then have ui - uj = - 1 if xij = 1, and always ui - uj ≦ p - 1. The above integer program involves n2 + n constraints (if t is not fixed) in n2 + 2n variables. Since the inequality form of constraint is fundamental for integer programming calculations, one may eliminate 2n variables, say the xi0 and x0j, by means of the equation constraints and produce an equivalent problem with n2 + n inequalities and n2 variables. The currently known integer programming procedures are sufficiently regular in their behavior to cast doubt on the heuristic value of machine experiments with our model. However, it seems appropriate to report the results of the five machine experiments we have conducted so far. The solution procedure used was the all-integer algorithm of R. E. Gomory [3] without the ranking procedure he describes. The first three experiments were simple model verification tests on a four-city standard traveling salesman problem with distance matrix [ 20 23 4 30 7 27 25 5 25 3 21 26 ] The first experiment was with a model, now obsolete, using roughly twice as many constraints and variables as the current model (for this problem, 28 constraints in 21 variables). The machine was halted after 4000 pivot steps had failed to produce a solution. The second experiment used the earlier model with the xi0 and x0j eliminated, resulting in a 28-constraint, 15-variable problem. Here the machine produced the optimal solution in 41 pivot steps. The third experiment used the current formulation with the xi0 and x0j eliminated, yielding 13 constraints and 9 variables. The optimal solution was reached in 7 pivot steps. The fourth and fifth experiments were used on a standard ten-city problem, due to Barachet, solved by Dantzig, Johnson and Fulkerson [1]. The current formulation was used, yielding 91 constraints in 81 variables. The fifth problem differed from the fourth only in that the ordering of the rows was altered to attempt to introduce more favorable pivot choices. In each case the machine was stopped after over 250 pivot steps had failed to produce the solution. In each case the last 100 pivot steps had failed to change the value of the objective function. It seems hopeful that more efficient integer programming procedures now under development will yield a satisfactory algorithmic solution to the traveling salesman problem, when applied to this model. In any case, the model serves to illustrate how problems of this sort may be succinctly formulated in integer programming terms.", + "authors": [ + { + "name": "C. E. Miller", + "publication_start": 1960, + "publication_end": 1960, + "publication_count": 1, + "citation_count": 179, + "available_for_download": 1, + "downloads_6_weeks": 130, + "downloads_12_months": 1004, + "downloads_cumulative": 9792, + "average_downloads_per_article": 9792, + "average_citations_per_article": 179 + }, + { + "name": "A. W. Tucker", + "publication_start": 1960, + "publication_end": 1993, + "publication_count": 5, + "citation_count": 196, + "available_for_download": 1, + "downloads_6_weeks": 130, + "downloads_12_months": 1004, + "downloads_cumulative": 9792, + "average_downloads_per_article": 9792, + "average_citations_per_article": 39.2 + }, + { + "name": "R. A. Zemlin", + "publication_start": 1960, + "publication_end": 1964, + "publication_count": 2, + "citation_count": 188, + "available_for_download": 2, + "downloads_6_weeks": 130, + "downloads_12_months": 1009, + "downloads_cumulative": 10023, + "average_downloads_per_article": 5011.5, + "average_citations_per_article": 94 + } + ] +} +{ + "url": "https://dl.acm.org/citation.cfm?id=2412979", + "title": "STRUCT programming analysis system", + "abstract": "The STRUCT system utilizes the flexibility of a powerful graphics display system to provide a set of tools for program analysis. These tools allow the analysis of the static prograin structure and the dynamic execution behavior. of programs within the entire operating system/user program environment of the Brown University Graphics System (BUGS). Information is collected and presented in a manner which fully exploits two aspects of this environment. First, the operating system has been developed in a well-structured hierarcal manner following principles laid down by other researchers (2), (3). Second the programs under analysis have been written in a structured programming language following coding conventions which make available, at the source code level, valuable program control information. A new set of pictorial constructs is introduced for presenting a. program structure (static or dynamic) for inspection. These constructs combine the best features of an indented structured source code listing and the box odented nature of traditional flow charts. The graphical tools available are USed to provide for swift changes in. the desired level of detail displayed within a program structure, for traveling linearly through a program structure, for traveling through a complex program structure (following subroutine or system calls), for concurrently viewing multiple related program structures, and for presenting dynamic program behavior data using three-dimensional projections, The volume of a three-dimensional box representing a program block is proportional to the block's resource utilization. The scope of this paper is limited to a description of the STRUCT system. This system is currently being used to predict and analyze the performance advantages available through the migration of function (program modules) between levels of software and between software and firmware within BUGS. The results of this research on migration will be included in a doctoral dissertation currently being written.", + "authors": [ + { + "name": "Andries Van Dam", + "publication_start": 1975, + "publication_end": 1975, + "publication_count": 1, + "citation_count": 0, + "available_for_download": 0, + "downloads_6_weeks": 8, + "downloads_12_months": 97, + "downloads_cumulative": 97, + "average_downloads_per_article": 0, + "average_citations_per_article": 0 + } + ] +} diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py index fcffeac13..02c6d8b74 100644 --- a/src/scraping/buxton/scraper.py +++ b/src/scraping/buxton/scraper.py @@ -116,22 +116,22 @@ def write_image(folder, name): data_doc_guid = guid() view_doc_guid = guid() + image = Image.open(f"{dist}/{folder}/{name}") + native_width, native_height = image.size + view_doc = { "_id": view_doc_guid, "fields": { "proto": protofy(data_doc_guid), "x": 10, "y": 10, - "width": 300, + "width": min(800, native_width), "zIndex": 2, "libraryBrush": False }, "__type": "Doc" } - image = Image.open(f"{dist}/{folder}/{name}") - native_width, native_height = image.size - data_doc = { "_id": data_doc_guid, "fields": { diff --git a/src/server/index.ts b/src/server/index.ts index e645e29b4..91aacdb98 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -43,6 +43,8 @@ import { Response } from 'express-serve-static-core'; 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)); @@ -139,6 +141,7 @@ app.get("/pull", (req, res) => })); // SEARCH +const solrURL = "http://localhost:8983/solr/#/dash"; // GETTERS @@ -516,8 +519,8 @@ function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => v const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", "string": "_t", - // "boolean": "_b", - // "image": ["_t", "url"], + "boolean": "_b", + "image": ["_t", "url"], "video": ["_t", "url"], "pdf": ["_t", "url"], "audio": ["_t", "url"], diff --git a/src/server/updateSearch.ts b/src/server/updateSearch.ts index de1fd25e1..f5de00978 100644 --- a/src/server/updateSearch.ts +++ b/src/server/updateSearch.ts @@ -6,8 +6,8 @@ import pLimit from 'p-limit'; const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", "string": "_t", - // "boolean": "_b", - // "image": ["_t", "url"], + "boolean": "_b", + "image": ["_t", "url"], "video": ["_t", "url"], "pdf": ["_t", "url"], "audio": ["_t", "url"], @@ -92,9 +92,21 @@ async function update() { } } await cursor.forEach(updateDoc); - await Promise.all(updates.map(update => { - return limit(() => Search.Instance.updateDocument(update)); - })); + for (let i = 0; i < updates.length; i++) { + console.log(i); + const result = await Search.Instance.updateDocument(updates[i]); + try { + console.log(JSON.parse(result).responseHeader.status); + } catch { + console.log("Error:"); + console.log(updates[i]); + console.log(result); + console.log("\n"); + } + } + // await Promise.all(updates.map(update => { + // return limit(() => Search.Instance.updateDocument(update)); + // })); cursor.close(); } |