diff options
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/DragManager.ts | 13 | ||||
| -rw-r--r-- | src/client/util/Import & Export/DirectoryImportBox.tsx | 376 | ||||
| -rw-r--r-- | src/client/util/Import & Export/ImportMetadataEntry.tsx | 149 | ||||
| -rw-r--r-- | src/client/util/LinkManager.ts | 38 | ||||
| -rw-r--r-- | src/client/util/RichTextSchema.tsx | 11 | ||||
| -rw-r--r-- | src/client/util/Scripting.ts | 45 | ||||
| -rw-r--r-- | src/client/util/SearchUtil.ts | 57 | ||||
| -rw-r--r-- | src/client/util/SelectionManager.ts | 17 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.scss | 2 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.tsx | 38 | ||||
| -rw-r--r-- | src/client/util/UndoManager.ts | 1 | ||||
| -rw-r--r-- | src/client/util/request-image-size.js | 6 |
12 files changed, 692 insertions, 61 deletions
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 7dc48fb78..cb71db2c5 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -41,9 +41,14 @@ export function SetupDrag( 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()]); + e.persist(); + CollectionDockingView.Instance.StartOtherDrag({ + pageX: e.pageX, + pageY: e.pageY, + preventDefault: emptyFunction, + button: 0 + }, [await docFunc()]); } else { document.addEventListener("pointermove", onRowMove); document.addEventListener("pointerup", onRowUp); @@ -315,11 +320,13 @@ export namespace DragManager { scaleYs.push(scaleY); let dragElement = ele.cloneNode(true) as HTMLElement; dragElement.style.opacity = "0.7"; + dragElement.style.borderRadius = getComputedStyle(ele).borderRadius; dragElement.style.position = "absolute"; dragElement.style.margin = "0"; dragElement.style.top = "0"; dragElement.style.bottom = ""; dragElement.style.left = "0"; + dragElement.style.transition = "none"; dragElement.style.color = "black"; dragElement.style.transformOrigin = "0 0"; dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000"; @@ -447,7 +454,7 @@ export namespace DragManager { x: e.x, y: e.y, data: dragData, - mods: e.altKey ? "AltKey" : "" + mods: e.altKey ? "AltKey" : e.ctrlKey ? "CtrlKey" : "" } }) ); diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx new file mode 100644 index 000000000..ce95ba90e --- /dev/null +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -0,0 +1,376 @@ +import "fs"; +import React = require("react"); +import { Doc, Opt, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { DocServer } from "../../DocServer"; +import { RouteStore } from "../../../server/RouteStore"; +import { action, observable, autorun, runInAction, computed } from "mobx"; +import { FieldViewProps, FieldView } from "../../views/nodes/FieldView"; +import Measure, { ContentRect } from "react-measure"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowUp, faTag, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { Docs, DocumentOptions } from "../../documents/Documents"; +import { observer } from "mobx-react"; +import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry"; +import { Utils } from "../../../Utils"; +import { DocumentManager } from "../DocumentManager"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import { Cast, BoolCast, NumCast } from "../../../new_fields/Types"; +import { listSpec } from "../../../new_fields/Schema"; + +const unsupported = ["text/html", "text/plain"]; + +@observer +export default class DirectoryImportBox extends React.Component<FieldViewProps> { + private selector = React.createRef<HTMLInputElement>(); + @observable private top = 0; + @observable private left = 0; + private dimensions = 50; + + @observable private entries: ImportMetadataEntry[] = []; + + @observable private quota = 1; + @observable private remaining = 1; + + @observable private uploading = false; + @observable private removeHover = false; + + public static LayoutString() { return FieldView.LayoutString(DirectoryImportBox); } + + constructor(props: FieldViewProps) { + super(props); + library.add(faArrowUp, faTag, faPlus); + let doc = this.props.Document; + this.editingMetadata = this.editingMetadata || false; + this.persistent = this.persistent || false; + !Cast(doc.data, listSpec(Doc)) && (doc.data = new List<Doc>()); + } + + @computed + private get editingMetadata() { + return BoolCast(this.props.Document.editingMetadata); + } + + private set editingMetadata(value: boolean) { + this.props.Document.editingMetadata = value; + } + + @computed + private get persistent() { + return BoolCast(this.props.Document.persistent); + } + + private set persistent(value: boolean) { + this.props.Document.persistent = value; + } + + handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => { + runInAction(() => this.uploading = true); + + let promises: Promise<void>[] = []; + let docs: Doc[] = []; + + let files = e.target.files; + if (!files || files.length === 0) return; + + let directory = (files.item(0) as any).webkitRelativePath.split("/", 1); + + let validated: File[] = []; + for (let i = 0; i < files.length; i++) { + let file = files.item(i); + file && !unsupported.includes(file.type) && validated.push(file); + } + + runInAction(() => this.quota = validated.length); + + let sizes = []; + let modifiedDates = []; + + for (let uploaded_file of validated) { + let formData = new FormData(); + formData.append('file', uploaded_file); + let dropFileName = uploaded_file ? uploaded_file.name : "-empty-"; + let type = uploaded_file.type; + + sizes.push(uploaded_file.size); + modifiedDates.push(uploaded_file.lastModified); + + runInAction(() => this.remaining++); + + let prom = fetch(DocServer.prepend(RouteStore.upload), { + method: 'POST', + body: formData + }).then(async (res: Response) => { + (await res.json()).map(action((file: any) => { + let docPromise = Docs.getDocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName }); + docPromise.then(doc => { + doc && docs.push(doc) && runInAction(() => this.remaining--); + }); + })); + }); + promises.push(prom); + } + + await Promise.all(promises); + + for (let i = 0; i < docs.length; i++) { + let doc = docs[i]; + doc.size = sizes[i]; + doc.modified = modifiedDates[i]; + this.entries.forEach(entry => { + let target = entry.onDataDoc ? Doc.GetProto(doc) : doc; + target[entry.key] = entry.value; + }); + } + + let doc = this.props.Document; + let height: number = NumCast(doc.height) || 0; + let offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0; + let options: DocumentOptions = { + title: `Import of ${directory}`, + width: 1105, + height: 500, + x: NumCast(doc.x), + y: NumCast(doc.y) + offset + }; + let parent = this.props.ContainingCollectionView; + if (parent) { + let importContainer = Docs.StackingDocument(docs, options); + importContainer.singleColumn = false; + Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); + !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); + DocumentManager.Instance.jumpToDocument(importContainer, true); + + } + + runInAction(() => { + this.uploading = false; + this.quota = 1; + this.remaining = 1; + }); + } + + componentDidMount() { + this.selector.current!.setAttribute("directory", ""); + this.selector.current!.setAttribute("webkitdirectory", ""); + } + + @action + preserveCentering = (rect: ContentRect) => { + let bounds = rect.offset!; + if (bounds.width === 0 || bounds.height === 0) { + return; + } + let offset = this.dimensions / 2; + this.left = bounds.width / 2 - offset; + this.top = bounds.height / 2 - offset; + } + + @action + addMetadataEntry = async () => { + let entryDoc = new Doc(); + entryDoc.checked = false; + entryDoc.key = keyPlaceholder; + entryDoc.value = valuePlaceholder; + Doc.AddDocToList(this.props.Document, "data", entryDoc); + } + + @action + remove = async (entry: ImportMetadataEntry) => { + let metadata = await DocListCastAsync(this.props.Document.data); + if (metadata) { + let index = this.entries.indexOf(entry); + if (index !== -1) { + runInAction(() => this.entries.splice(index, 1)); + index = metadata.indexOf(entry.props.Document); + if (index !== -1) { + metadata.splice(index, 1); + } + } + + } + } + + render() { + let dimensions = 50; + let entries = DocListCast(this.props.Document.data); + let isEditing = this.editingMetadata; + let remaining = this.remaining; + let quota = this.quota; + let uploading = this.uploading; + let showRemoveLabel = this.removeHover; + let persistent = this.persistent; + let percent = `${100 - (remaining / quota * 100)}`; + percent = percent.split(".")[0]; + percent = percent.startsWith("100") ? "99" : percent; + let marginOffset = (percent.length === 1 ? 5 : 0) - 1.6; + return ( + <Measure offset onResize={this.preserveCentering}> + {({ measureRef }) => + <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} > + <input + id={"selector"} + ref={this.selector} + onChange={this.handleSelection} + type="file" + style={{ + position: "absolute", + display: "none" + }} /> + <label + htmlFor={"selector"} + style={{ + opacity: isEditing ? 0 : 1, + pointerEvents: isEditing ? "none" : "all", + transition: "0.4s ease opacity" + }} + > + <div style={{ + width: dimensions, + height: dimensions, + borderRadius: "50%", + background: "black", + position: "absolute", + left: this.left, + top: this.top + }} /> + <div style={{ + position: "absolute", + left: this.left + 12.6, + top: this.top + 11, + opacity: uploading ? 0 : 1, + transition: "0.4s opacity ease" + }}> + <FontAwesomeIcon icon={faArrowUp} color="#FFFFFF" size={"2x"} /> + </div> + <img + style={{ + width: 80, + height: 80, + transition: "0.4s opacity ease", + opacity: uploading ? 0.7 : 0, + position: "absolute", + top: this.top - 15, + left: this.left - 15 + }} + src={"/assets/loading.gif"}></img> + </label> + <input + type={"checkbox"} + onChange={e => runInAction(() => this.persistent = e.target.checked)} + style={{ + margin: 0, + position: "absolute", + left: 10, + bottom: 10, + opacity: isEditing || uploading ? 0 : 1, + transition: "0.4s opacity ease", + pointerEvents: isEditing || uploading ? "none" : "all" + }} + checked={this.persistent} + onPointerEnter={action(() => this.removeHover = true)} + onPointerLeave={action(() => this.removeHover = false)} + /> + <p + style={{ + position: "absolute", + left: 27, + bottom: 8.4, + fontSize: 12, + opacity: showRemoveLabel ? 1 : 0, + transition: "0.4s opacity ease" + }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p> + <div + style={{ + transition: "0.4s opacity ease", + opacity: uploading ? 1 : 0, + pointerEvents: "none", + position: "absolute", + left: 10, + top: this.top + 12.3, + fontSize: 18, + color: "white", + marginLeft: this.left + marginOffset + }}>{percent}%</div> + <div + style={{ + position: "absolute", + top: 10, + right: 10, + borderRadius: "50%", + width: 25, + height: 25, + background: "black", + pointerEvents: uploading ? "none" : "all", + opacity: uploading ? 0 : 1, + transition: "0.4s opacity ease" + }} + title={isEditing ? "Back to Upload" : "Add Metadata"} + onClick={action(() => this.editingMetadata = !this.editingMetadata)} + /> + <FontAwesomeIcon + style={{ + pointerEvents: "none", + position: "absolute", + right: isEditing ? 16.3 : 14.5, + top: isEditing ? 15.4 : 16, + opacity: uploading ? 0 : 1, + transition: "0.4s opacity ease" + }} + icon={isEditing ? faArrowUp : faTag} + color="#FFFFFF" + size={"1x"} + /> + <div + style={{ + transition: "0.4s ease opacity", + width: "100%", + height: "100%", + pointerEvents: isEditing ? "all" : "none", + opacity: isEditing ? 1 : 0, + overflowY: "scroll" + }} + > + <div + style={{ + borderRadius: "50%", + width: 25, + height: 25, + marginLeft: 10, + position: "absolute", + right: 41, + top: 10 + }} + title={"Add Metadata Entry"} + onClick={this.addMetadataEntry} + > + <FontAwesomeIcon + style={{ + pointerEvents: "none", + marginLeft: 6.4, + marginTop: 5.2 + }} + icon={faPlus} + size={"1x"} + /> + </div> + <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }} >Add metadata to your import...</p> + <hr style={{ margin: "6px 10px 12px 10px" }} /> + {entries.map(doc => + <ImportMetadataEntry + Document={doc} + key={doc[Id]} + remove={this.remove} + ref={(el) => { if (el) this.entries.push(el); }} + next={this.addMetadataEntry} + /> + )} + </div> + </div> + } + </Measure> + ); + } + +}
\ No newline at end of file diff --git a/src/client/util/Import & Export/ImportMetadataEntry.tsx b/src/client/util/Import & Export/ImportMetadataEntry.tsx new file mode 100644 index 000000000..f5198c39b --- /dev/null +++ b/src/client/util/Import & Export/ImportMetadataEntry.tsx @@ -0,0 +1,149 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import { EditableView } from "../../views/EditableView"; +import { observable, action, computed } from "mobx"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { Opt, Doc } from "../../../new_fields/Doc"; +import { StrCast, BoolCast } from "../../../new_fields/Types"; + +interface KeyValueProps { + Document: Doc; + remove: (self: ImportMetadataEntry) => void; + next: () => void; +} + +export const keyPlaceholder = "Key"; +export const valuePlaceholder = "Value"; + +@observer +export default class ImportMetadataEntry extends React.Component<KeyValueProps> { + + private keyRef = React.createRef<EditableView>(); + private valueRef = React.createRef<EditableView>(); + private checkRef = React.createRef<HTMLInputElement>(); + + constructor(props: KeyValueProps) { + super(props); + library.add(faPlus); + } + + @computed + public get valid() { + return (this.key.length > 0 && this.key !== keyPlaceholder) && (this.value.length > 0 && this.value !== valuePlaceholder); + } + + @computed + private get backing() { + return this.props.Document; + } + + @computed + public get onDataDoc() { + return BoolCast(this.backing.checked); + } + + public set onDataDoc(value: boolean) { + this.backing.checked = value; + } + + @computed + public get key() { + return StrCast(this.backing.key); + } + + public set key(value: string) { + this.backing.key = value; + } + + @computed + public get value() { + return StrCast(this.backing.value); + } + + public set value(value: string) { + this.backing.value = value; + } + + @action + updateKey = (newKey: string) => { + this.key = newKey; + this.keyRef.current && this.keyRef.current.setIsFocused(false); + this.valueRef.current && this.valueRef.current.setIsFocused(true); + this.key.length === 0 && (this.key = keyPlaceholder); + return true; + } + + @action + updateValue = (newValue: string, shiftDown: boolean) => { + this.value = newValue; + this.valueRef.current && this.valueRef.current.setIsFocused(false); + this.value.length > 0 && shiftDown && this.props.next(); + this.value.length === 0 && (this.value = valuePlaceholder); + return true; + } + + render() { + let keyValueStyle: React.CSSProperties = { + paddingLeft: 10, + width: "50%", + opacity: this.valid ? 1 : 0.5, + }; + return ( + <div + style={{ + display: "flex", + flexDirection: "row", + paddingBottom: 5, + paddingRight: 5, + justifyContent: "center", + alignItems: "center", + alignContent: "center" + }} + > + <input + onChange={e => this.onDataDoc = e.target.checked} + ref={this.checkRef} + style={{ margin: "0 10px 0 15px" }} + type="checkbox" + title={"Add to Data Document?"} + checked={this.onDataDoc} + /> + <div className={"key_container"} style={keyValueStyle}> + <EditableView + ref={this.keyRef} + contents={this.key} + SetValue={this.updateKey} + GetValue={() => ""} + oneLine={true} + /> + </div> + <div + className={"value_container"} + style={keyValueStyle}> + <EditableView + ref={this.valueRef} + contents={this.value} + SetValue={this.updateValue} + GetValue={() => ""} + oneLine={true} + /> + </div> + <div onClick={() => this.props.remove(this)} title={"Delete Entry"}> + <FontAwesomeIcon + icon={faPlus} + color={"red"} + size={"1x"} + style={{ + marginLeft: 15, + marginRight: 15, + transform: "rotate(45deg)" + }} + /> + </div> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index f2f3e51dd..944bc532f 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -68,8 +68,8 @@ export class LinkManager { // finds all links that contain the given anchor public getAllRelatedLinks(anchor: Doc): Doc[] {//List<Doc> { let related = LinkManager.Instance.getAllLinks().filter(link => { - let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc)); - let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc)); + let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null)); + let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null)); return protomatch1 || protomatch2; }); return related; @@ -100,9 +100,11 @@ export class LinkManager { if (index > -1) groupTypes.splice(index, 1); LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes); LinkManager.Instance.LinkManagerDoc[groupType] = undefined; - LinkManager.Instance.getAllLinks().forEach(linkDoc => { - LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType); - LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc), groupType); + LinkManager.Instance.getAllLinks().forEach(async linkDoc => { + const anchor1 = await Cast(linkDoc.anchor1, Doc); + const anchor2 = await Cast(linkDoc.anchor2, Doc); + anchor1 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor1, groupType); + anchor2 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor2, groupType); }); } return true; @@ -122,8 +124,8 @@ export class LinkManager { } // gets the groups associates with an anchor in a link - public getAnchorGroups(linkDoc: Doc, anchor: Doc): Array<Doc> { - if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) { + public getAnchorGroups(linkDoc: Doc, anchor?: Doc): Array<Doc> { + if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) { return DocListCast(linkDoc.anchor1Groups); } else { return DocListCast(linkDoc.anchor2Groups); @@ -132,7 +134,7 @@ export class LinkManager { // sets the groups of the given anchor in the given link public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) { - if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) { + if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) { linkDoc.anchor1Groups = new List<Doc>(groups); } else { linkDoc.anchor2Groups = new List<Doc>(groups); @@ -209,10 +211,10 @@ export class LinkManager { let md: Doc[] = []; let allLinks = LinkManager.Instance.getAllLinks(); allLinks.forEach(linkDoc => { - let anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc)); - let anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc)); - anchor1Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) md.push(Cast(groupDoc.metadata, Doc, new Doc)); }); - anchor2Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) md.push(Cast(groupDoc.metadata, Doc, new Doc)); }); + let anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, null)); + let anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, null)); + anchor1Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } }); + anchor2Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } }); }); return md; } @@ -221,18 +223,20 @@ export class LinkManager { public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean { let allLinks = LinkManager.Instance.getAllLinks(); let index = allLinks.findIndex(linkDoc => { - return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, new Doc), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, new Doc), anchor2)) || - (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, new Doc), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, new Doc), anchor1)); + return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) || + (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1)); }); return index !== -1; } // finds the opposite anchor of a given anchor in a link + //TODO This should probably return undefined if there isn't an opposite anchor + //TODO This should also await the return value of the anchor so we don't filter out promises public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc { - if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) { - return Cast(linkDoc.anchor2, Doc, new Doc); + if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) { + return Cast(linkDoc.anchor2, Doc, null)!; } else { - return Cast(linkDoc.anchor1, Doc, new Doc); + return Cast(linkDoc.anchor1, Doc, null)!; } } }
\ No newline at end of file diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 9197a3b6f..a2842ca42 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -357,6 +357,17 @@ export const marks: { [index: string]: MarkSpec } = { }] }, + pFontColor: { + attrs: { + color: { default: "yellow" } + }, + parseDOM: [{ style: 'background: #d9dbdd' }], + toDOM: (node) => { + return ['span', { + style: `color: ${node.attrs.color}` + }]; + } + }, /** FONT SIZES */ pFontSize: { diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 30a05154a..3156c4f43 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -7,12 +7,7 @@ let ts = (window as any).ts; // @ts-ignore import * as typescriptlib from '!!raw-loader!./type_decls.d'; -import { Docs } from "../documents/Documents"; import { Doc, Field } from '../../new_fields/Doc'; -import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField'; -import { List } from '../../new_fields/List'; -import { RichTextField } from '../../new_fields/RichTextField'; -import { ScriptField, ComputedField } from '../../new_fields/ScriptField'; export interface ScriptSucccess { success: true; @@ -38,6 +33,34 @@ export interface CompileError { errors: any[]; } +export namespace Scripting { + export function addGlobal(global: { name: string }): void; + export function addGlobal(name: string, global: any): void; + export function addGlobal(nameOrGlobal: any, global?: any) { + let n: string; + let obj: any; + if (global !== undefined && typeof nameOrGlobal === "string") { + n = nameOrGlobal; + obj = global; + } else if (nameOrGlobal && typeof nameOrGlobal.name === "string") { + n = nameOrGlobal.name; + obj = nameOrGlobal; + } else { + throw new Error("Must either register an object with a name, or give a name and an object"); + } + if (scriptingGlobals.hasOwnProperty(n)) { + throw new Error(`Global with name ${n} is already registered, choose another name`); + } + scriptingGlobals[n] = obj; + } +} + +export function scriptingGlobal(constructor: { new(...args: any[]): any }) { + Scripting.addGlobal(constructor); +} + +const scriptingGlobals: { [name: string]: any } = {}; + export type CompileResult = CompiledScript | CompileError; function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); @@ -45,9 +68,11 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an return { compiled: false, errors: diagnostics }; } - let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript]; - let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)]; - let params: any[] = [Docs, ...fieldTypes]; + let paramNames = Object.keys(scriptingGlobals); + let params = paramNames.map(key => scriptingGlobals[key]); + // let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript]; + // let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)]; + // let params: any[] = [Docs, ...fieldTypes]; let compiledFunction = new Function(...paramNames, `return ${script}`); let { capturedVariables = {} } = options; let run = (args: { [name: string]: any } = {}): ScriptResult => { @@ -178,4 +203,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); return Run(outputText, paramNames, diagnostics, script, options); -}
\ No newline at end of file +} + +Scripting.addGlobal(CompileScript);
\ No newline at end of file diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 27d27a3b8..338628960 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -4,27 +4,64 @@ import { Doc } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; export namespace SearchUtil { - export function Search(query: string, returnDocs: true): Promise<Doc[]>; - export function Search(query: string, returnDocs: false): Promise<string[]>; + export interface IdSearchResult { + ids: string[]; + numFound: number; + } + + export interface DocSearchResult { + docs: Doc[]; + numFound: number; + } + + export function Search(query: string, returnDocs: true): Promise<DocSearchResult>; + export function Search(query: string, returnDocs: false): Promise<IdSearchResult>; export async function Search(query: string, returnDocs: boolean) { - const ids = JSON.parse(await rp.get(DocServer.prepend("/search"), { + const result: IdSearchResult = JSON.parse(await rp.get(DocServer.prepend("/search"), { qs: { query } })); if (!returnDocs) { - return ids; + return result; } + const { ids, numFound } = result; const docMap = await DocServer.GetRefFields(ids); - return ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc); + const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc); + return { docs, numFound }; } - export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]> { - const proto = await Doc.GetT(doc, "proto", Doc, true); - const protoId = (proto || doc)[Id]; - return Search(`proto_i:"${protoId}"`, true); + export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>; + export async function GetAliasesOfDocument(doc: Doc, returnDocs: false): Promise<string[]>; + export async function GetAliasesOfDocument(doc: Doc, returnDocs = true): Promise<Doc[] | string[]> { + const proto = Doc.GetProto(doc); + const protoId = proto[Id]; + if (returnDocs) { + return (await Search(`proto_i:"${protoId}"`, returnDocs)).docs; + } else { + return (await Search(`proto_i:"${protoId}"`, returnDocs)).ids; + } // return Search(`{!join from=id to=proto_i}id:${protoId}`, true); } export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> { - return Search(`proto_i:"${doc[Id]}"`, true); + const results = await Search(`proto_i:"${doc[Id]}"`, true); + return results.docs; + } + + export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[], aliasContexts: Doc[] }> { + const docContexts = (await Search(`data_l:"${doc[Id]}"`, true)).docs; + const aliases = await GetAliasesOfDocument(doc, false); + const aliasContexts = (await Promise.all(aliases.map(doc => Search(`data_l:"${doc}"`, true)))); + const contexts = { contexts: docContexts, aliasContexts: [] as Doc[] }; + aliasContexts.forEach(result => contexts.aliasContexts.push(...result.docs)); + return contexts; + } + + export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[], aliasContexts: string[] }> { + const docContexts = (await Search(`data_l:"${doc[Id]}"`, false)).ids; + const aliases = await GetAliasesOfDocument(doc, false); + const aliasContexts = (await Promise.all(aliases.map(doc => Search(`data_l:"${doc}"`, false)))); + const contexts = { contexts: docContexts, aliasContexts: [] as string[] }; + aliasContexts.forEach(result => contexts.aliasContexts.push(...result.ids)); + return contexts; } }
\ No newline at end of file diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 3bc71ad42..9efef888d 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,8 +1,9 @@ import { observable, action, runInAction, IReactionDisposer, reaction, autorun } from "mobx"; -import { Doc } from "../../new_fields/Doc"; +import { Doc, Opt } from "../../new_fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; -import { NumCast } from "../../new_fields/Types"; +import { NumCast, StrCast } from "../../new_fields/Types"; +import { InkingControl } from "../views/InkingControl"; export namespace SelectionManager { @@ -11,6 +12,7 @@ export namespace SelectionManager { @observable IsDragging: boolean = false; @observable SelectedDocuments: Array<DocumentView> = []; + @action SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it @@ -41,6 +43,17 @@ export namespace SelectionManager { } const manager = new Manager(); + reaction(() => manager.SelectedDocuments, sel => { + let targetColor = "#FFFFFF"; + if (sel.length > 0) { + let firstView = sel[0]; + let doc = firstView.props.Document; + let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc); + let stored = StrCast(targetDoc.backgroundColor); + stored.length > 0 && (targetColor = stored); + } + InkingControl.Instance.updateSelectedColor(targetColor); + }, { fireImmediately: true }); export function DeselectDoc(docView: DocumentView): void { manager.DeselectDoc(docView); diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index 38870a1ce..d460da3c8 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -249,7 +249,7 @@ transform: translateY(-85px); pointer-events: all; height: 30px; - width:500px; + width:550px; .ProseMirror-example-setup-style hr { padding: 2px 10px; border: none; diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 42d72e441..515d9439c 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -32,7 +32,7 @@ export class TooltipTextMenu { private fontSizeToNum: Map<MarkType, number>; private fontStylesToName: Map<MarkType, string>; private listTypeToIcon: Map<NodeType, string>; - private link: HTMLAnchorElement; + //private link: HTMLAnchorElement; //private wrapper: HTMLDivElement; private linkEditor?: HTMLDivElement; @@ -92,6 +92,7 @@ export class TooltipTextMenu { }); }); + this.updateLinkMenu(); //list of font styles this.fontStylesToName = new Map(); @@ -114,7 +115,8 @@ export class TooltipTextMenu { this.fontSizeToNum.set(schema.marks.p32, 32); this.fontSizeToNum.set(schema.marks.p48, 48); this.fontSizeToNum.set(schema.marks.p72, 72); - //this.fontSizeToNum.set(schema.marks.pFontSize,schema.marks.pFontSize.) + this.fontSizeToNum.set(schema.marks.pFontSize, 10); + this.fontSizeToNum.set(schema.marks.pFontSize, 10); this.fontSizes = Array.from(this.fontSizeToNum.keys()); //list types @@ -123,11 +125,6 @@ export class TooltipTextMenu { this.listTypeToIcon.set(schema.nodes.ordered_list, "1)"); this.listTypes = Array.from(this.listTypeToIcon.keys()); - this.link = document.createElement("a"); - this.link.target = "_blank"; - this.link.style.color = "white"; - //this.tooltip.appendChild(this.link); - this.tooltip.appendChild(this.createLink().render(this.view).dom); this.tooltip.appendChild(this.createStar().render(this.view).dom); @@ -234,6 +231,7 @@ export class TooltipTextMenu { updateLinkMenu() { if (!this.linkEditor || !this.linkText) { this.linkEditor = document.createElement("div"); + this.linkEditor.className = "ProseMirror-icon menuicon"; this.linkEditor.style.color = "black"; this.linkText = document.createElement("div"); this.linkText.style.cssFloat = "left"; @@ -274,8 +272,9 @@ export class TooltipTextMenu { }; this.linkDrag = document.createElement("img"); this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png"; - this.linkDrag.style.width = "20px"; - this.linkDrag.style.height = "20px"; + this.linkDrag.style.width = "15px"; + this.linkDrag.style.height = "15px"; + this.linkDrag.title = "Drag to create link"; this.linkDrag.style.color = "black"; this.linkDrag.style.background = "black"; this.linkDrag.style.cssFloat = "left"; @@ -293,10 +292,10 @@ export class TooltipTextMenu { hideSource: false }); }; - // this.linkEditor.appendChild(this.linkDrag); + this.linkEditor.appendChild(this.linkDrag); // this.linkEditor.appendChild(this.linkText); // this.linkEditor.appendChild(linkBtn); - //this.tooltip.appendChild(this.linkEditor); + this.tooltip.appendChild(this.linkEditor); } let node = this.view.state.selection.$from.nodeAfter; @@ -487,16 +486,24 @@ export class TooltipTextMenu { enable(state) { return !state.selection.empty; }, run: (state, dispatch, view) => { // to remove link + let curLink = ""; if (this.markActive(state, markType)) { - toggleMark(markType)(state, dispatch); - return true; + + let { from, $from, to, empty } = state.selection; + let node = state.doc.nodeAt(from); + node && node.marks.map(m => { + m.type === markType && (curLink = m.attrs.href); + }) + //toggleMark(markType)(state, dispatch); + //return true; } // to create link openPrompt({ title: "Create a link", fields: { href: new TextField({ - label: "Link target", + value: curLink, + label: "Link Target", required: true }), title: new TextField({ label: "Title" }) @@ -646,9 +653,6 @@ export class TooltipTextMenu { } } this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks)); - this.updateLinkMenu(); - //this.highlightSearchTerms(["hello", "there"]); - //this.unhighlightSearchTerms(); } //finds all active marks on selection in given group diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index c0ed015bd..156390fd3 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -94,6 +94,7 @@ export namespace UndoManager { } export function PrintBatches(): void { + console.log("Open Undo Batches:"); GetOpenBatches().forEach(batch => console.log(batch.batchName)); } diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.js index f6fe1068a..257990811 100644 --- a/src/client/util/request-image-size.js +++ b/src/client/util/request-image-size.js @@ -21,7 +21,9 @@ module.exports = function requestImageSize(options) { if (options && typeof options === 'object') { opts = Object.assign(options, opts); } else if (options && typeof options === 'string') { - opts = Object.assign({ uri: options }, opts); + opts = Object.assign({ + uri: options + }, opts); } else { return Promise.reject(new Error('You should provide an URI string or a "request" options object.')); } @@ -70,4 +72,4 @@ module.exports = function requestImageSize(options) { req.on('error', err => reject(err)); }); -}; +};
\ No newline at end of file |
