diff options
55 files changed, 1636 insertions, 682 deletions
diff --git a/package.json b/package.json index f0f2b467e..f869713ba 100644 --- a/package.json +++ b/package.json @@ -172,18 +172,17 @@ "passport-local": "^1.0.0", "pdfjs-dist": "^2.0.943", "probe-image-size": "^4.0.0", - "prosemirror": "^0.11.1", - "prosemirror-commands": "^1.0.7", + "prosemirror-commands": "^1.0.8", "prosemirror-example-setup": "^1.0.1", "prosemirror-find-replace": "^0.9.0", "prosemirror-history": "^1.0.4", "prosemirror-keymap": "^1.0.1", - "prosemirror-model": "^1.7.0", - "prosemirror-schema-basic": "^1.0.0", - "prosemirror-schema-list": "^1.0.2", - "prosemirror-state": "^1.2.2", - "prosemirror-transform": "^1.1.3", - "prosemirror-view": "^1.8.3", + "prosemirror-model": "^1.7.2", + "prosemirror-schema-basic": "^1.0.1", + "prosemirror-schema-list": "^1.0.3", + "prosemirror-state": "^1.2.4", + "prosemirror-transform": "^1.1.4", + "prosemirror-view": "^1.10.3", "pug": "^2.0.3", "query-string": "^6.8.1", "raw-loader": "^1.0.0", diff --git a/src/Utils.ts b/src/Utils.ts index 71d88683a..ec2bee9bf 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -58,6 +58,98 @@ export class Utils { document.body.removeChild(textArea); } + public static fromRGBAstr(rgba: string) { + let rm = rgba.match(/rgb[a]?\(([0-9]+)/); + let r = rm ? Number(rm[1]) : 0; + let gm = rgba.match(/rgb[a]?\([0-9]+,([0-9]+)/); + let g = gm ? Number(gm[1]) : 0; + let bm = rgba.match(/rgb[a]?\([0-9]+,[0-9]+,([0-9]+)/); + let b = bm ? Number(bm[1]) : 0; + let am = rgba.match(/rgba?\([0-9]+,[0-9]+,[0-9]+,([0-9]+)/); + let a = am ? Number(am[1]) : 0; + return { r: r, g: g, b: b, a: a }; + } + public static toRGBAstr(col: { r: number, g: number, b: number, a?: number }) { + return "rgba(" + col.r + "," + col.g + "," + col.b + (col.a !== undefined ? "," + col.a : "") + ")"; + } + + public static HSLtoRGB(h: number, s: number, l: number) { + // Must be fractions of 1 + // s /= 100; + // l /= 100; + + let c = (1 - Math.abs(2 * l - 1)) * s, + x = c * (1 - Math.abs((h / 60) % 2 - 1)), + m = l - c / 2, + r = 0, + g = 0, + b = 0; + if (0 <= h && h < 60) { + r = c; g = x; b = 0; + } else if (60 <= h && h < 120) { + r = x; g = c; b = 0; + } else if (120 <= h && h < 180) { + r = 0; g = c; b = x; + } else if (180 <= h && h < 240) { + r = 0; g = x; b = c; + } else if (240 <= h && h < 300) { + r = x; g = 0; b = c; + } else if (300 <= h && h < 360) { + r = c; g = 0; b = x; + } + r = Math.round((r + m) * 255); + g = Math.round((g + m) * 255); + b = Math.round((b + m) * 255); + return { r: r, g: g, b: b }; + } + + public static RGBToHSL(r: number, g: number, b: number) { + // Make r, g, and b fractions of 1 + r /= 255; + g /= 255; + b /= 255; + + // Find greatest and smallest channel values + let cmin = Math.min(r, g, b), + cmax = Math.max(r, g, b), + delta = cmax - cmin, + h = 0, + s = 0, + l = 0; + // Calculate hue + + // No difference + if (delta == 0) + h = 0; + // Red is max + else if (cmax == r) + h = ((g - b) / delta) % 6; + // Green is max + else if (cmax == g) + h = (b - r) / delta + 2; + // Blue is max + else + h = (r - g) / delta + 4; + + h = Math.round(h * 60); + + // Make negative hues positive behind 360° + if (h < 0) + h += 360; // Calculate lightness + + l = (cmax + cmin) / 2; + + // Calculate saturation + s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); + + // Multiply l and s by 100 + // s = +(s * 100).toFixed(1); + // l = +(l * 100).toFixed(1); + + return { h: h, s: s, l: l }; + } + + public static GetClipboardText(): string { var textArea = document.createElement("textarea"); document.body.appendChild(textArea); @@ -138,6 +230,20 @@ export function WithKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => vo return dup; } +export function timenow() { + var now = new Date(); + let ampm = 'am'; + let h = now.getHours(); + let m: any = now.getMinutes(); + let s: any = now.getSeconds(); + if (h >= 12) { + if (h > 12) h -= 12; + ampm = 'pm'; + } + if (m < 10) m = '0' + m; + return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm; +} + export function numberRange(num: number) { return Array.from(Array(num)).map((v, i) => i); } export function returnTrue() { return true; } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 9bac57d16..5dd945c16 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -66,6 +66,7 @@ export interface DocumentOptions { page?: number; scale?: number; layout?: string; + isTemplate?: boolean; templates?: List<string>; viewType?: number; backgroundColor?: string; @@ -424,8 +425,8 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options }); } - export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }); + export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }, id); } export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) { @@ -614,13 +615,13 @@ export namespace Docs { export namespace DocUtils { - export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc) { + export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string) { if (LinkManager.Instance.doesLinkExist(source, target)) return undefined; let sv = DocumentManager.Instance.getDocumentView(source); if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return; if (target === CurrentUserUtils.UserDocument) return undefined; - let linkDocProto = new Doc(); + let linkDocProto = new Doc(id, true); UndoManager.RunInBatch(() => { linkDocProto.type = DocumentType.LINK; diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 348f216a5..b0bbb5462 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -92,16 +92,26 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps> let sizes = []; let modifiedDates = []; - let formData = new FormData(); - for (let uploaded_file of validated) { - formData.append(Utils.GenerateGuid(), uploaded_file); - sizes.push(uploaded_file.size); - modifiedDates.push(uploaded_file.lastModified); - runInAction(() => this.remaining++); + let i = 0; + const uploads: FileResponse[] = []; + const batchSize = 15; + + while (i < validated.length) { + const cap = Math.min(validated.length, i + batchSize); + let formData = new FormData(); + const batch = validated.slice(i, cap); + + sizes.push(...batch.map(file => file.size)); + modifiedDates.push(...batch.map(file => file.lastModified)); + + batch.forEach(file => formData.append(Utils.GenerateGuid(), file)); + const parameters = { method: 'POST', body: formData }; + uploads.push(...(await (await fetch(Utils.prepend(RouteStore.upload), parameters)).json())); + + runInAction(() => this.remaining += batch.length); + i = cap; } - const parameters = { method: 'POST', body: formData }; - const uploads: FileResponse[] = await (await fetch(Utils.prepend(RouteStore.upload), parameters)).json(); await Promise.all(uploads.map(async upload => { const type = upload.type; @@ -138,7 +148,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps> }; let parent = this.props.ContainingCollectionView; if (parent) { - let importContainer = Docs.Create.StackingDocument(docs, options); + let importContainer = Docs.Create.MasonryDocument(docs, options); await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); importContainer.singleColumn = false; Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); diff --git a/src/client/util/ParagraphNodeSpec.ts b/src/client/util/ParagraphNodeSpec.ts new file mode 100644 index 000000000..3a993e1ff --- /dev/null +++ b/src/client/util/ParagraphNodeSpec.ts @@ -0,0 +1,133 @@ +import clamp from './clamp'; +import convertToCSSPTValue from './convertToCSSPTValue'; +import toCSSLineSpacing from './toCSSLineSpacing'; +import { Node, DOMOutputSpec } from 'prosemirror-model'; + +//import type { NodeSpec } from './Types'; +type NodeSpec = { + attrs?: { [key: string]: any }, + content?: string, + draggable?: boolean, + group?: string, + inline?: boolean, + name?: string, + parseDOM?: Array<any>, + toDOM?: (node: any) => DOMOutputSpec, +}; + +// This assumes that every 36pt maps to one indent level. +export const INDENT_MARGIN_PT_SIZE = 36; +export const MIN_INDENT_LEVEL = 0; +export const MAX_INDENT_LEVEL = 7; +export const ATTRIBUTE_INDENT = 'data-indent'; + +export const EMPTY_CSS_VALUE = new Set(['', '0%', '0pt', '0px']); + +const ALIGN_PATTERN = /(left|right|center|justify)/; + +// https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js +// :: NodeSpec A plain paragraph textblock. Represented in the DOM +// as a `<p>` element. +const ParagraphNodeSpec: NodeSpec = { + attrs: { + align: { default: null }, + color: { default: null }, + id: { default: null }, + indent: { default: null }, + lineSpacing: { default: null }, + // TODO: Add UI to let user edit / clear padding. + paddingBottom: { default: null }, + // TODO: Add UI to let user edit / clear padding. + paddingTop: { default: null }, + }, + content: 'inline*', + group: 'block', + parseDOM: [{ tag: 'p', getAttrs }], + toDOM, +}; + +function getAttrs(dom: HTMLElement): Object { + const { + lineHeight, + textAlign, + marginLeft, + paddingTop, + paddingBottom, + } = dom.style; + + let align = dom.getAttribute('align') || textAlign || ''; + align = ALIGN_PATTERN.test(align) ? align : ""; + + let indent = parseInt(dom.getAttribute(ATTRIBUTE_INDENT) || "", 10); + + if (!indent && marginLeft) { + indent = convertMarginLeftToIndentValue(marginLeft); + } + + indent = indent || MIN_INDENT_LEVEL; + + const lineSpacing = lineHeight ? toCSSLineSpacing(lineHeight) : null; + + const id = dom.getAttribute('id') || ''; + return { align, indent, lineSpacing, paddingTop, paddingBottom, id }; +} + +function toDOM(node: Node): DOMOutputSpec { + const { + align, + indent, + lineSpacing, + paddingTop, + paddingBottom, + id, + } = node.attrs; + const attrs: { [key: string]: any } | null = {}; + + let style = ''; + if (align && align !== 'left') { + style += `text-align: ${align};`; + } + + if (lineSpacing) { + const cssLineSpacing = toCSSLineSpacing(lineSpacing); + style += + `line-height: ${cssLineSpacing};` + + // This creates the local css variable `--czi-content-line-height` + // that its children may apply. + `--czi-content-line-height: ${cssLineSpacing}`; + } + + if (paddingTop && !EMPTY_CSS_VALUE.has(paddingTop)) { + style += `padding-top: ${paddingTop};`; + } + + if (paddingBottom && !EMPTY_CSS_VALUE.has(paddingBottom)) { + style += `padding-bottom: ${paddingBottom};`; + } + + style && (attrs.style = style); + + if (indent) { + attrs[ATTRIBUTE_INDENT] = String(indent); + } + + if (id) { + attrs.id = id; + } + + return ['p', attrs, 0]; +} + +export const toParagraphDOM = toDOM; +export const getParagraphNodeAttrs = getAttrs; + +export function convertMarginLeftToIndentValue(marginLeft: string): number { + const ptValue = convertToCSSPTValue(marginLeft); + return clamp( + MIN_INDENT_LEVEL, + Math.floor(ptValue / INDENT_MARGIN_PT_SIZE), + MAX_INDENT_LEVEL + ); +} + +export default ParagraphNodeSpec;
\ No newline at end of file diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts index 419311df8..1d2d33800 100644 --- a/src/client/util/ProsemirrorExampleTransfer.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -2,8 +2,8 @@ import { chainCommands, exitCode, joinDown, joinUp, lift, selectParentNode, setB import { redo, undo } from "prosemirror-history"; import { undoInputRule } from "prosemirror-inputrules"; import { Schema } from "prosemirror-model"; -import { liftListItem, } from "./prosemirrorPatches.js"; -import { splitListItem, wrapInList, sinkListItem } from "prosemirror-schema-list"; +import { liftListItem, sinkListItem } from "./prosemirrorPatches.js"; +import { splitListItem, wrapInList, } from "prosemirror-schema-list"; import { EditorState, Transaction, TextSelection, NodeSelection } from "prosemirror-state"; import { TooltipTextMenu } from "./TooltipTextMenu"; @@ -51,18 +51,18 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: bind("Ctrl->", wrapIn(schema.nodes.blockquote)); - bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - let newNode = schema.nodes.footnote.create({}); - if (dispatch && state.selection.from === state.selection.to) { - let tr = state.tr; - tr.replaceSelectionWith(newNode); // replace insertion with a footnote. - dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display - tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node) - tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)))); - return true; - } - return false; - }) + // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + // let newNode = schema.nodes.footnote.create({}); + // if (dispatch && state.selection.from === state.selection.to) { + // let tr = state.tr; + // tr.replaceSelectionWith(newNode); // replace insertion with a footnote. + // dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display + // tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node) + // tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)))); + // return true; + // } + // return false; + // }); let cmd = chainCommands(exitCode, (state, dispatch) => { @@ -93,49 +93,31 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: bind("Mod-s", TooltipTextMenu.insertStar); - let updateBullets = (tx2: Transaction, refStart: number, delta: number) => { - for (let i = refStart; i > 0; i--) { - let testPos = tx2.doc.nodeAt(i); - if (testPos && testPos.type === schema.nodes.list_item) { - let start = i; - let preve = i > 0 && tx2.doc.nodeAt(start - 1); - if (preve && preve.type === schema.nodes.ordered_list) { - start = start - 1; - } - let rangeStart = tx2.doc.nodeAt(start); - if (rangeStart && rangeStart.type === schema.nodes.ordered_list) { - tx2.setNodeMarkup(start, rangeStart.type, { ...rangeStart.attrs, bulletStyle: rangeStart.attrs.bulletStyle + delta }, rangeStart.marks); - } - rangeStart && rangeStart.descendants((node: any, offset: any, index: any) => { - if (node.type === schema.nodes.ordered_list) { - tx2.setNodeMarkup(start + offset + 1, node.type, { ...node.attrs, bulletStyle: node.attrs.bulletStyle + delta }, node.marks); - } - }); - break; + let updateBullets = (tx2: Transaction) => { + tx2.doc.descendants((node: any, offset: any, index: any) => { + if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) { + let path = (tx2.doc.resolve(offset) as any).path; + let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && (c as any).type === schema.nodes.ordered_list ? 1 : 0), 0); + if (node.type === schema.nodes.ordered_list) depth++; + tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: node.attrs.mapStyle, bulletStyle: depth }, node.marks); } - } - } + }); + }; + bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { var ref = state.selection; var range = ref.$from.blockRange(ref.$to); var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => { - updateBullets(tx2, range!.start, 1); + updateBullets(tx2); marks && tx2.ensureMarks([...marks]); marks && tx2.setStoredMarks([...marks]); dispatch(tx2); })) { // couldn't sink into an existing list, so wrap in a new one - let sxf = state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)); - let newstate = state.applyTransaction(sxf); + let newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end))); if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => { - for (let i = range!.start; i >= 0; i--) { - let rangeStart = tx2.doc.nodeAt(i); - if (rangeStart && rangeStart.type === schema.nodes.ordered_list) { - tx2.setNodeMarkup(i, rangeStart.type, { ...rangeStart.attrs, bulletStyle: 1 }, rangeStart.marks); - break; - } - } + updateBullets(tx2); // when promoting to a list, assume list will format things so don't copy the stored marks. marks && tx2.ensureMarks([...marks]); marks && tx2.setStoredMarks([...marks]); @@ -147,13 +129,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: }); bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - var range = state.selection.$from.blockRange(state.selection.$to); var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); - let tr = state.tr; - updateBullets(tr, range!.start, -1); - - if (!liftListItem(schema.nodes.list_item)(tr, (tx2: Transaction) => { + if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => { + updateBullets(tx2); marks && tx2.ensureMarks([...marks]); marks && tx2.setStoredMarks([...marks]); dispatch(tx2); @@ -162,16 +141,16 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: } }); + let splitMetadata = (marks: any, tx: Transaction) => { + marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); + marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); + return tx; + } bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); - if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => { - // marks && tx3.ensureMarks(marks); - // marks && tx3.setStoredMarks(marks); - dispatch(tx3); - })) { + if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => dispatch(tx3))) { if (!splitBlockKeepMarks(state, (tx3: Transaction) => { - marks && tx3.ensureMarks(marks); - marks && tx3.setStoredMarks(marks); + splitMetadata(marks, tx3); if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) { dispatch(tx3); } @@ -181,6 +160,27 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: } return true; }); + bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); + dispatch(splitMetadata(marks, state.tr)); + return false; + }); + bind(":", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + let range = state.selection.$from.blockRange(state.selection.$to, (node: any) => { + return !node.marks || !node.marks.find((m: any) => m.type === schema.marks.metadata); + }); + let path = (state.doc.resolve(state.selection.from - 1) as any).path; + let spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1; + let textsel = TextSelection.create(state.doc, range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator, range!.end); + let text = range ? state.doc.textBetween(textsel.from, textsel.to) : ""; + let whitespace = text.length - 1; + for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { } + if (text.endsWith(":")) { + dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any). + addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any)); + } + return false; + }); return keys; diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts index 8c4c76027..00e671db9 100644 --- a/src/client/util/RichTextRules.ts +++ b/src/client/util/RichTextRules.ts @@ -1,14 +1,10 @@ -import { - inputRules, - wrappingInputRule, - textblockTypeInputRule, - smartQuotes, - emDash, - ellipsis -} from "prosemirror-inputrules"; -import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType } from "prosemirror-model"; - +import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from "prosemirror-inputrules"; import { schema } from "./RichTextSchema"; +import { wrappingInputRule } from "./prosemirrorPatches"; +import { NodeSelection } from "prosemirror-state"; +import { NumCast, Cast } from "../../new_fields/Types"; +import { Doc } from "../../new_fields/Doc"; +import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; export const inpRules = { rules: [ @@ -21,17 +17,29 @@ export const inpRules = { // 1. ordered list wrappingInputRule( - /^(\d+)\.\s$/, + /^1\.\s$/, schema.nodes.ordered_list, - match => ({ order: +match[1] }), - (match, node) => node.childCount + node.attrs.order === +match[1] + () => { + return ({ mapStyle: "decimal", bulletStyle: 1 }) + }, + (match: any, node: any) => { + return node.childCount + node.attrs.order === +match[1]; + }, + (type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } }) ), // a. alphabbetical list wrappingInputRule( - /^([a-z]+)\.\s$/, - schema.nodes.alphabet_list, - match => ({ order: +match[1] }), - (match, node) => node.childCount + node.attrs.order === +match[1] + /^a\.\s$/, + schema.nodes.ordered_list, + // match => { + () => { + return ({ mapStyle: "alpha", bulletStyle: 1 }) + // return ({ order: +match[1] }) + }, + (match: any, node: any) => { + return node.childCount + node.attrs.order === +match[1]; + }, + (type: any) => ({ type: type, attrs: { mapStyle: "alpha", bulletStyle: 1 } }) ), // * bullet list @@ -42,9 +50,76 @@ export const inpRules = { // # heading textblockTypeInputRule( - new RegExp("^(#{1,6})\\s$"), + new RegExp(/^(#{1,6})\s$/), schema.nodes.heading, - match => ({ level: match[1].length }) - ) + match => { + return ({ level: match[1].length }); + } + ), + + new InputRule( + new RegExp(/^#([0-9]+)\s$/), + (state, match, start, end) => { + let size = Number(match[1]); + let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc; + let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + if (ruleProvider && heading) { + ruleProvider["ruleSize_" + heading] = size; + } + return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) })) + }), + new InputRule( + new RegExp(/^\^\^\s$/), + (state, match, start, end) => { + let node = (state.doc.resolve(start) as any).nodeAfter; + let sm = state.storedMarks || undefined; + let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc; + let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + if (ruleProvider && heading) { + ruleProvider["ruleAlign_" + heading] = "center"; + } + return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + }), + new InputRule( + new RegExp(/^\[\[\s$/), + (state, match, start, end) => { + let node = (state.doc.resolve(start) as any).nodeAfter; + let sm = state.storedMarks || undefined; + let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc; + let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + if (ruleProvider && heading) { + ruleProvider["ruleAlign_" + heading] = "left"; + } + return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + }), + new InputRule( + new RegExp(/^\]\]\s$/), + (state, match, start, end) => { + let node = (state.doc.resolve(start) as any).nodeAfter; + let sm = state.storedMarks || undefined; + let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc; + let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + if (ruleProvider && heading) { + ruleProvider["ruleAlign_" + heading] = "right"; + } + return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + }), + new InputRule( + new RegExp(/\^f\s$/), + (state, match, start, end) => { + let newNode = schema.nodes.footnote.create({}); + let tr = state.tr; + tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote. + return tr.setSelection(new NodeSelection( // select the footnote node to open its display + tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node) + tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))); + }), + // let newNode = schema.nodes.footnote.create({}); + // if (dispatch && state.selection.from === state.selection.to) { + // return true; + // } ] }; diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 25d972857..f027a4bf7 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -1,12 +1,18 @@ -import { DOMOutputSpecArray, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model"; +import { baseKeymap, toggleMark } from "prosemirror-commands"; +import { redo, undo } from "prosemirror-history"; +import { keymap } from "prosemirror-keymap"; +import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model"; import { bulletList, listItem, orderedList } from 'prosemirror-schema-list'; -import { TextSelection, EditorState } from "prosemirror-state"; -import { Doc } from "../../new_fields/Doc"; +import { EditorState, TextSelection } from "prosemirror-state"; import { StepMap } from "prosemirror-transform"; import { EditorView } from "prosemirror-view"; -import { keymap } from "prosemirror-keymap"; -import { undo, redo } from "prosemirror-history"; -import { toggleMark, splitBlock, selectAll, baseKeymap } from "prosemirror-commands"; +import { Doc } from "../../new_fields/Doc"; +import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; +import { DocServer } from "../DocServer"; +import { Cast, NumCast } from "../../new_fields/Types"; +import { DocumentManager } from "./DocumentManager"; +import ParagraphNodeSpec from "./ParagraphNodeSpec"; +import { times } from "async"; const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; @@ -19,6 +25,7 @@ export const nodes: { [index: string]: NodeSpec } = { content: "block+" }, + footnote: { group: "inline", content: "inline*", @@ -33,23 +40,17 @@ export const nodes: { [index: string]: NodeSpec } = { parseDOM: [{ tag: "footnote" }] }, - // :: NodeSpec A plain paragraph textblock. Represented in the DOM - // as a `<p>` element. - paragraph: { - content: "inline*", - group: "block", - parseDOM: [{ tag: "p" }], - toDOM() { return pDOM; } - }, - - // starmine: { - // inline: true, - // attrs: { oldtext: { default: "" } }, - // group: "inline", - // toDOM() { return ["star", "㊉"]; }, - // parseDOM: [{ tag: "star" }] + // // :: NodeSpec A plain paragraph textblock. Represented in the DOM + // // as a `<p>` element. + // paragraph: { + // content: "inline*", + // group: "block", + // parseDOM: [{ tag: "p" }], + // toDOM() { return pDOM; } // }, + paragraph: ParagraphNodeSpec, + // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. blockquote: { content: "block+", @@ -107,8 +108,6 @@ export const nodes: { [index: string]: NodeSpec } = { visibility: { default: false }, text: { default: undefined }, textslice: { default: undefined }, - textlen: { default: 0 } - }, group: "inline", toDOM(node) { @@ -132,9 +131,11 @@ export const nodes: { [index: string]: NodeSpec } = { inline: true, attrs: { src: {}, - width: { default: "100px" }, + width: { default: 100 }, alt: { default: null }, - title: { default: null } + title: { default: null }, + float: { default: "left" }, + docid: { default: "" } }, group: "inline", draggable: true, @@ -148,7 +149,7 @@ export const nodes: { [index: string]: NodeSpec } = { }; } }], - // TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why? + // TODO if we don't define toDom, dragging the image crashes. Why? toDOM(node) { const attrs = { style: `width: ${node.attrs.width}` }; return ["img", { ...node.attrs, ...attrs }]; @@ -197,46 +198,43 @@ export const nodes: { [index: string]: NodeSpec } = { attrs: { bulletStyle: { default: 0 }, mapStyle: { default: "decimal" }, + visibility: { default: true } }, toDOM(node: Node<any>) { const bs = node.attrs.bulletStyle; const decMap = bs ? "decimal" + bs : ""; const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : ""; let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap; - for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = map; - return ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0]; - //return node.attrs.bulletStyle < 2 ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] : - // ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle}; font-size: 5px` }, "hello"]; + return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] : + ['ol', { class: `${map}-ol`, style: `list-style: none;` }]; } }, - //this doesn't currently work for some reason + bullet_list: { ...bulletList, content: 'list_item+', group: 'block', // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }], toDOM(node: Node<any>) { - for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = ""; return ['ul', 0]; } }, - //bullet_list: { - // content: 'list_item+', - // group: 'block', - //active: blockActive(schema.nodes.bullet_list), - //enable: wrapInList(schema.nodes.bullet_list), - //run: wrapInList(schema.nodes.bullet_list), - //select: state => true, - // }, list_item: { attrs: { - className: { default: "" } + bulletStyle: { default: 0 }, + mapStyle: { default: "decimal" }, + visibility: { default: true } }, ...listItem, content: 'paragraph block*', toDOM(node: any) { - return ["li", { class: node.attrs.className }, 0]; + const bs = node.attrs.bulletStyle; + const decMap = bs ? "decimal" + bs : ""; + const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : ""; + let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap; + return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."]; + //return ["li", { class: `${map}` }, 0]; } }, }; @@ -244,7 +242,6 @@ export const nodes: { [index: string]: NodeSpec } = { const emDOM: DOMOutputSpecArray = ["em", 0]; const strongDOM: DOMOutputSpecArray = ["strong", 0]; const codeDOM: DOMOutputSpecArray = ["code", 0]; -const underlineDOM: DOMOutputSpecArray = ["underline", 0]; // :: Object [Specs](#model.MarkSpec) for the marks in the schema. export const marks: { [index: string]: MarkSpec } = { @@ -255,7 +252,8 @@ export const marks: { [index: string]: MarkSpec } = { attrs: { href: {}, location: { default: null }, - title: { default: null } + title: { default: null }, + docref: { default: false } }, inclusive: false, parseDOM: [{ @@ -263,7 +261,11 @@ export const marks: { [index: string]: MarkSpec } = { return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title") }; } }], - toDOM(node: any) { return ["a", node.attrs, 0]; } + toDOM(node: any) { + return node.attrs.docref && node.attrs.title ? + ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] : + ["a", { ...node.attrs }, 0]; + } }, // :: MarkSpec An emphasis mark. Rendered as an `<em>` element. @@ -282,16 +284,6 @@ export const marks: { [index: string]: MarkSpec } = { toDOM() { return strongDOM; } }, - underline: { - parseDOM: [ - { tag: 'u' }, - { style: 'text-decoration=underline' } - ], - toDOM: () => ['span', { - style: 'text-decoration:underline' - }] - }, - strikethrough: { parseDOM: [ { tag: 'strike' }, @@ -332,15 +324,68 @@ export const marks: { [index: string]: MarkSpec } = { } }, + metadata: { + toDOM() { + return ['span', { style: 'font-size:75%; background:rgba(100, 100, 100, 0.2); ' }]; + } + }, + metadataKey: { + toDOM() { + return ['span', { style: 'font-style:italic; ' }]; + } + }, + metadataVal: { + toDOM() { + return ['span']; + } + }, + highlight: { - parseDOM: [{ style: 'text-decoration: underline' }], + parseDOM: [ + { + tag: "span", + getAttrs: (p: any) => { + if (typeof (p) !== "string") { + let style = getComputedStyle(p); + if (style.textDecoration === "underline") return null; + if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 && + p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) + return null; + } + return false; + } + }, + ], + inclusive: true, toDOM() { return ['span', { - style: 'text-decoration: underline; text-decoration-color: rgba(204, 206, 210, 0.92)' + style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)' }]; } }, + underline: { + parseDOM: [ + { + tag: "span", + getAttrs: (p: any) => { + if (typeof (p) !== "string") { + let style = getComputedStyle(p); + if (style.textDecoration === "underline") + return null; + if (p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1) + return null; + } + return false; + } + } + // { style: "text-decoration=underline" } + ], + toDOM: () => ['span', { + style: 'text-decoration:underline;text-decoration-style:line' + }] + }, + search_highlight: { parseDOM: [{ style: 'background: yellow' }], toDOM() { @@ -359,7 +404,6 @@ export const marks: { [index: string]: MarkSpec } = { modified: { default: "when?" } }, group: "inline", - inclusive: false, toDOM(node: any) { let hideUsers = node.attrs.hide_users; let hidden = hideUsers.indexOf(node.attrs.userid) !== -1 || (hideUsers.length === 0 && node.attrs.userid !== Doc.CurrentUserEmail); @@ -379,6 +423,24 @@ export const marks: { [index: string]: MarkSpec } = { toDOM() { return codeDOM; } }, + // pFontFamily: { + // attrs: { + // style: { default: 'font-family: "Times New Roman", Times, serif;' }, + // }, + // parseDOM: [{ + // tag: "span", getAttrs(dom: any) { + // if (getComputedStyle(dom).font === "Times New Roman") return { style: `font-family: "Times New Roman", Times, serif;` }; + // if (getComputedStyle(dom).font === "Arial, Helvetica") return { style: `font-family: Arial, Helvetica, sans-serif;` }; + // if (getComputedStyle(dom).font === "Georgia") return { style: `font-family: Georgia, serif;` }; + // if (getComputedStyle(dom).font === "Comic Sans") return { style: `font-family: "Comic Sans MS", cursive, sans-serif;` }; + // if (getComputedStyle(dom).font === "Tahoma, Geneva") return { style: `font-family: Tahoma, Geneva, sans-serif;` }; + // } + // }], + // toDOM: (node: any) => ['span', { + // style: node.attrs.style + // }] + // }, + /* FONTS */ timesNewRoman: { @@ -523,15 +585,12 @@ export const marks: { [index: string]: MarkSpec } = { }] }, }; -function getFontSize(element: any) { - return parseFloat((getComputedStyle(element) as any).fontSize); -} export class ImageResizeView { _handle: HTMLElement; _img: HTMLElement; _outer: HTMLElement; - constructor(node: any, view: any, getPos: any) { + constructor(node: any, view: any, getPos: any, addDocTab: any) { this._handle = document.createElement("span"); this._img = document.createElement("img"); this._outer = document.createElement("span"); @@ -539,6 +598,7 @@ export class ImageResizeView { this._outer.style.width = node.attrs.width; this._outer.style.display = "inline-block"; this._outer.style.overflow = "hidden"; + (this._outer.style as any).float = node.attrs.float; this._img.setAttribute("src", node.attrs.src); this._img.style.width = "100%"; @@ -551,6 +611,33 @@ export class ImageResizeView { this._handle.style.bottom = "-10px"; this._handle.style.right = "-10px"; let self = this; + this._img.onpointerdown = function (e: any) { + if (!view.isOverlay || e.ctrlKey) { + e.preventDefault(); + e.stopPropagation(); + DocServer.GetRefField(node.attrs.docid).then(async linkDoc => { + if (linkDoc instanceof Doc) { + let proto = Doc.GetProto(linkDoc); + let targetContext = await Cast(proto.targetContext, Doc); + let jumpToDoc = await Cast(linkDoc.anchor2, Doc); + if (jumpToDoc) { + if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { + + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page))); + return; + } + } + if (targetContext) { + DocumentManager.Instance.jumpToDocument(targetContext, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab")); + } else if (jumpToDoc) { + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab")); + } else { + DocumentManager.Instance.jumpToDocument(linkDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab")); + } e.ctrlKey + } + }); + } + } this._handle.onpointerdown = function (e: any) { e.preventDefault(); e.stopPropagation(); @@ -560,15 +647,18 @@ export class ImageResizeView { const currentX = e.pageX; const diffInPx = currentX - startX; self._outer.style.width = `${startWidth + diffInPx}`; + //Array.from(FormattedTextBox.InputBoxOverlay!.CurrentDiv.getElementsByTagName("img")).map((img: any) => img.opacity = "0.1"); + FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "0"; }; const onpointerup = () => { document.removeEventListener("pointermove", onpointermove); document.removeEventListener("pointerup", onpointerup); view.dispatch( - view.state.tr.setNodeMarkup(getPos(), null, - { src: node.attrs.src, width: self._outer.style.width }) - .setSelection(view.state.selection)); + view.state.tr.setSelection(view.state.selection).setNodeMarkup(getPos(), null, + { ...node.attrs, width: self._outer.style.width }) + ); + FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "1"; }; document.addEventListener("pointermove", onpointermove); @@ -594,8 +684,6 @@ export class ImageResizeView { } export class OrderedListView { - constructor(node: any, view: any, getPos: any) { } - update(node: any) { return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update } @@ -610,33 +698,33 @@ export class FootnoteView { constructor(node: any, view: any, getPos: any) { // We'll need these later - this.node = node - this.outerView = view - this.getPos = getPos + this.node = node; + this.outerView = view; + this.getPos = getPos; // The node's representation in the editor (empty, for now) this.dom = document.createElement("footnote"); this.dom.addEventListener("pointerup", this.toggle, true); // These are used when the footnote is selected - this.innerView = null + this.innerView = null; } selectNode() { const attrs = { ...this.node.attrs }; attrs.visibility = true; - this.dom.classList.add("ProseMirror-selectednode") - if (!this.innerView) this.open() + this.dom.classList.add("ProseMirror-selectednode"); + if (!this.innerView) this.open(); } deselectNode() { const attrs = { ...this.node.attrs }; attrs.visibility = false; - this.dom.classList.remove("ProseMirror-selectednode") - if (this.innerView) this.close() + this.dom.classList.remove("ProseMirror-selectednode"); + if (this.innerView) this.close(); } open() { - if (!(this.outerView as any).isOverlay) return; + if (!this.outerView.isOverlay) return; // Append a tooltip to the outer node - let tooltip = this.dom.appendChild(document.createElement("div")) + let tooltip = this.dom.appendChild(document.createElement("div")); tooltip.className = "footnote-tooltip"; // And put a sub-ProseMirror into that this.innerView = new EditorView(tooltip, { @@ -676,126 +764,105 @@ export class FootnoteView { if (this.innerView) this.close(); else { this.open(); - } } close() { - this.innerView && this.innerView.destroy() - this.innerView = null - this.dom.textContent = "" + this.innerView && this.innerView.destroy(); + this.innerView = null; + this.dom.textContent = ""; } dispatchInner(tr: any) { - let { state, transactions } = this.innerView.state.applyTransaction(tr) - this.innerView.updateState(state) + let { state, transactions } = this.innerView.state.applyTransaction(tr); + this.innerView.updateState(state); if (!tr.getMeta("fromOutside")) { let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1) for (let i = 0; i < transactions.length; i++) { - let steps = transactions[i].steps - for (let j = 0; j < steps.length; j++) - outerTr.step(steps[j].map(offsetMap)) + let steps = transactions[i].steps; + for (let j = 0; j < steps.length; j++) { + outerTr.step(steps[j].map(offsetMap)); + } } - if (outerTr.docChanged) this.outerView.dispatch(outerTr) + if (outerTr.docChanged) this.outerView.dispatch(outerTr); } } update(node: any) { - if (!node.sameMarkup(this.node)) return false - this.node = node + if (!node.sameMarkup(this.node)) return false; + this.node = node; if (this.innerView) { - let state = this.innerView.state - let start = node.content.findDiffStart(state.doc.content) - if (start != null) { - let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content) - let overlap = start - Math.min(endA, endB) - if (overlap > 0) { endA += overlap; endB += overlap } + let state = this.innerView.state; + let start = node.content.findDiffStart(state.doc.content); + if (start !== null) { + let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content); + let overlap = start - Math.min(endA, endB); + if (overlap > 0) { endA += overlap; endB += overlap; } this.innerView.dispatch( state.tr .replace(start, endB, node.slice(start, endA)) - .setMeta("fromOutside", true)) + .setMeta("fromOutside", true)); } } - return true + return true; } destroy() { - if (this.innerView) this.close() + if (this.innerView) this.close(); } stopEvent(event: any) { - return this.innerView && this.innerView.dom.contains(event.target) + return this.innerView && this.innerView.dom.contains(event.target); } - ignoreMutation() { return true } + ignoreMutation() { return true; } } export class SummarizedView { - // TODO: highlight text that is summarized. to find end of region, walk along mark _collapsed: HTMLElement; _view: any; constructor(node: any, view: any, getPos: any) { this._collapsed = document.createElement("span"); - this._collapsed.textContent = node.attrs.visibility ? "㊀" : "㊉"; - this._collapsed.style.opacity = "0.5"; - this._collapsed.style.position = "relative"; - this._collapsed.style.width = "40px"; - this._collapsed.style.height = "20px"; - let self = this; + this._collapsed.className = this.className(node.attrs.visibility); this._view = view; const js = node.toJSON; node.toJSON = function () { - return js.apply(this, arguments); }; - this._collapsed.onpointerdown = function (e: any) { - if (node.attrs.visibility) { - // node.attrs.visibility = !node.attrs.visibility; - let y = getPos(); - const attrs = { ...node.attrs }; - attrs.visibility = !attrs.visibility; - let { from, to } = self.updateSummarizedText(y + 1, view.state.schema.marks.highlight); - let length = to - from; - let newSelection = TextSelection.create(view.state.doc, y + 1, y + 1 + length); - // update attrs of node - attrs.text = newSelection.content(); - attrs.textslice = newSelection.content().toJSON(); - view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs)); - view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { })); - let marks = view.state.storedMarks.filter((m: any) => m.type !== view.state.schema.marks.highlight); - view.state.storedMarks = marks; - self._collapsed.textContent = "㊉"; - } else { - // node.attrs.visibility = !node.attrs.visibility; - let y = getPos(); - const attrs = { ...node.attrs }; - attrs.visibility = !attrs.visibility; - view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs)); - let mark = view.state.schema.mark(view.state.schema.marks.highlight); - view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1))); - const from = view.state.selection.from; - let size = node.attrs.text.size; - view.dispatch(view.state.tr.replaceSelection(node.attrs.text).addMark(from, from + size, mark).removeStoredMark(mark)); - self._collapsed.textContent = "㊀"; + + this._collapsed.onpointerdown = (e: any) => { + const visible = !node.attrs.visibility; + const attrs = { ...node.attrs, visibility: visible }; + let textSelection = TextSelection.create(view.state.doc, getPos() + 1); + if (!visible) { // update summarized text and save in attrs + textSelection = this.updateSummarizedText(getPos() + 1); + attrs.text = textSelection.content(); + attrs.textslice = attrs.text.toJSON(); } + view.dispatch(view.state.tr. + setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed) + replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it + setNodeMarkup(getPos(), undefined, attrs)); // update the attrs e.preventDefault(); e.stopPropagation(); + this._collapsed.className = this.className(visible); }; (this as any).dom = this._collapsed; - - } - selectNode() { } + selectNode() { } + + deselectNode() { } - updateSummarizedText(start?: any, mark?: any) { - let $start = this._view.state.doc.resolve(start); + className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed"); + + updateSummarizedText(start?: any) { + let mark = this._view.state.schema.marks.highlight.create(); let endPos = start; - let _mark = this._view.state.schema.mark(this._view.state.schema.marks.highlight); let visited = new Set(); for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) { let skip = false; this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => { if (node.isLeaf && !visited.has(node) && !skip) { - if (node.marks.find((m: any) => m.type === _mark.type)) { + if (node.marks.find((m: any) => m.type === mark.type)) { visited.add(node); endPos = i + node.nodeSize - 1; } @@ -803,10 +870,7 @@ export class SummarizedView { } }); } - return { from: start, to: endPos }; - } - - deselectNode() { + return TextSelection.create(this._view.state.doc, start, endPos); } } // :: Schema diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index ce7b04e31..c376b6f86 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -4,7 +4,7 @@ import { action, observable } from "mobx"; import { Dropdown, icons, MenuItem } from "prosemirror-menu"; //no import css import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model"; import { wrapInList } from 'prosemirror-schema-list'; -import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; +import { EditorState, NodeSelection, TextSelection, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Doc, Field, Opt } from "../../new_fields/Doc"; import { Id } from "../../new_fields/FieldSymbols"; @@ -18,6 +18,7 @@ import { DragManager } from "./DragManager"; import { LinkManager } from "./LinkManager"; import { schema } from "./RichTextSchema"; import "./TooltipTextMenu.scss"; +import { Cast, NumCast } from '../../new_fields/Types'; const { toggleMark, setBlockType } = require("prosemirror-commands"); const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js"); @@ -307,10 +308,9 @@ export class TooltipTextMenu { { handlers: { dragComplete: action(() => { - // let m = dragData.droppedDocuments; let linkDoc = dragData.linkDocument; let proto = Doc.GetProto(linkDoc); - if (docView && docView.props.ContainingCollectionView) { + if (proto && docView && docView.props.ContainingCollectionView) { proto.sourceContext = docView.props.ContainingCollectionView.props.Document; } linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab"); @@ -322,8 +322,6 @@ export class TooltipTextMenu { e.preventDefault(); }; this.linkEditor.appendChild(this.linkDrag); - // this.linkEditor.appendChild(this.linkText); - // this.linkEditor.appendChild(linkBtn); this.tooltip.appendChild(this.linkEditor); } @@ -432,11 +430,13 @@ export class TooltipTextMenu { } public static insertStar(state: EditorState<any>, dispatch: any) { - let newNode = schema.nodes.star.create({ visibility: false, text: state.selection.content(), textslice: state.selection.content().toJSON(), textlen: state.selection.to - state.selection.from }); - if (dispatch) { - //console.log(newNode.attrs.text.toString()); - dispatch(state.tr.replaceSelectionWith(newNode)); - } + if (state.selection.empty) return false; + let mark = state.schema.marks.highlight.create(); + let tr = state.tr; + tr.addMark(state.selection.from, state.selection.to, mark); + let content = tr.selection.content(); + let newNode = schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() }); + dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); return true; } @@ -496,10 +496,20 @@ export class TooltipTextMenu { if (markType.name[0] === 'p') { let size = this.fontSizeToNum.get(markType); if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } + let ruleProvider = Cast(this.editorProps.Document.ruleProvider, Doc) as Doc; + let heading = NumCast(this.editorProps.Document.heading); + if (ruleProvider && heading) { + ruleProvider["ruleSize_" + heading] = size; + } } else { let fontName = this.fontStylesToName.get(markType); if (fontName) { this.updateFontStyleDropdown(fontName); } + let ruleProvider = Cast(this.editorProps.Document.ruleProvider, Doc) as Doc; + let heading = NumCast(this.editorProps.Document.heading); + if (ruleProvider && heading) { + ruleProvider["ruleFont_" + heading] = fontName; + } } //actually apply font return toggleMark(markType)(view.state, view.dispatch, view); @@ -509,30 +519,37 @@ export class TooltipTextMenu { } } + updateBullets = (tx2: Transaction, style: string) => { + tx2.doc.descendants((node: any, offset: any, index: any) => { + if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) { + let path = (tx2.doc.resolve(offset) as any).path; + let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && (c as any).type === schema.nodes.ordered_list ? 1 : 0), 0); + if (node.type === schema.nodes.ordered_list) depth++; + tx2.setNodeMarkup(offset, node.type, { mapStyle: style, bulletStyle: depth }, node.marks); + } + }); + }; //remove all node typeand apply the passed-in one to the selected text - changeToNodeType(nodeType: NodeType | undefined, view: EditorView) { + changeToNodeType = (nodeType: NodeType | undefined, view: EditorView) => { //remove oldif (nodeType) { //add new if (nodeType === schema.nodes.bullet_list) { wrapInList(nodeType)(view.state, view.dispatch); } else { - var ref = view.state.selection; - var range = ref.$from.blockRange(ref.$to); var marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks()); - wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => { - const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2)); - let path = resolvedPos.path; - for (let i = path.length - 1; i > 0; i--) { - if (path[i].type === schema.nodes.ordered_list) { - path[i].attrs.bulletStyle = 1; - path[i].attrs.mapStyle = (nodeType as any).attrs.mapStyle; - break; - } - } + if (!wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => { + this.updateBullets(tx2, (nodeType as any).attrs.mapStyle); marks && tx2.ensureMarks([...marks]); marks && tx2.setStoredMarks([...marks]); view.dispatch(tx2); - }); + })) { + let tx2 = view.state.tr; + this.updateBullets(tx2, (nodeType as any).attrs.mapStyle); + marks && tx2.ensureMarks([...marks]); + marks && tx2.setStoredMarks([...marks]); + + view.dispatch(tx2); + } } } @@ -630,7 +647,6 @@ export class TooltipTextMenu { Array.from(this._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => { const markType = mark.type; this.changeToMarkInGroup(markType, this.view, []); - }); } } diff --git a/src/client/util/clamp.js b/src/client/util/clamp.js new file mode 100644 index 000000000..9c7fd78a4 --- /dev/null +++ b/src/client/util/clamp.js @@ -0,0 +1,15 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = clamp; +function clamp(min, val, max) { + if (val < min) { + return min; + } + if (val > max) { + return max; + } + return val; +}
\ No newline at end of file diff --git a/src/client/util/convertToCSSPTValue.js b/src/client/util/convertToCSSPTValue.js new file mode 100644 index 000000000..179557953 --- /dev/null +++ b/src/client/util/convertToCSSPTValue.js @@ -0,0 +1,43 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PT_TO_PX_RATIO = exports.PX_TO_PT_RATIO = undefined; +exports.default = convertToCSSPTValue; +exports.toClosestFontPtSize = toClosestFontPtSize; + +// var _FontSizeCommandMenuButton = require('./ui/FontSizeCommandMenuButton'); + +var SIZE_PATTERN = /([\d\.]+)(px|pt)/i; + +var PX_TO_PT_RATIO = exports.PX_TO_PT_RATIO = 0.7518796992481203; // 1 / 1.33. +var PT_TO_PX_RATIO = exports.PT_TO_PX_RATIO = 1.33; + +function convertToCSSPTValue(styleValue) { + var matches = styleValue.match(SIZE_PATTERN); + if (!matches) { + return 0; + } + var value = parseFloat(matches[1]); + var unit = matches[2]; + if (!value || !unit) { + return 0; + } + if (unit === 'px') { + value = PX_TO_PT_RATIO * value; + } + return value; +} + +function toClosestFontPtSize(styleValue) { + var originalPTValue = convertToCSSPTValue(styleValue); + + // if (_FontSizeCommandMenuButton.FONT_PT_SIZES.includes(originalPTValue)) { + // return originalPTValue; + // } + + return _FontSizeCommandMenuButton.FONT_PT_SIZES.reduce(function (prev, curr) { + return Math.abs(curr - originalPTValue) < Math.abs(prev - originalPTValue) ? curr : prev; + }, Number.NEGATIVE_INFINITY); +}
\ No newline at end of file diff --git a/src/client/util/prosemirrorPatches.js b/src/client/util/prosemirrorPatches.js index c273c2323..188e3e1c5 100644 --- a/src/client/util/prosemirrorPatches.js +++ b/src/client/util/prosemirrorPatches.js @@ -2,10 +2,13 @@ Object.defineProperty(exports, '__esModule', { value: true }); +var prosemirrorInputRules = require('prosemirror-inputrules'); var prosemirrorTransform = require('prosemirror-transform'); var prosemirrorModel = require('prosemirror-model'); exports.liftListItem = liftListItem; +exports.sinkListItem = sinkListItem; +exports.wrappingInputRule = wrappingInputRule; // :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool // Create a command to lift the list item around the selection up into // a wrapping list. @@ -59,4 +62,78 @@ function liftOutOfList(tr, dispatch, range) { atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1)); dispatch(tr.scrollIntoView()); return true +} + +// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool +// Create a command to sink the list item around the selection down +// into an inner list. +function sinkListItem(itemType) { + return function (state, dispatch) { + var ref = state.selection; + var $from = ref.$from; + var $to = ref.$to; + var range = $from.blockRange($to, function (node) { return node.childCount && node.firstChild.type == itemType; }); + if (!range) { return false } + var startIndex = range.startIndex; + if (startIndex == 0) { return false } + var parent = range.parent, nodeBefore = parent.child(startIndex - 1); + if (nodeBefore.type != itemType) { return false; } + + if (dispatch) { + var nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type; + var inner = prosemirrorModel.Fragment.from(nestedBefore ? itemType.create() : null); + let slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, prosemirrorModel.Fragment.from(parent.type.create(parent.attrs, inner)))), + nestedBefore ? 3 : 1, 0); + var before = range.start, after = range.end; + dispatch(state.tr.step(new prosemirrorTransform.ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after, + before, after, slice, 1, true)) + .scrollIntoView()); + } + return true + } +} + +function findWrappingOutside(range, type) { + var parent = range.parent; + var startIndex = range.startIndex; + var endIndex = range.endIndex; + var around = parent.contentMatchAt(startIndex).findWrapping(type); + if (!around) { return null } + var outer = around.length ? around[0] : type; + return parent.canReplaceWith(startIndex, endIndex, outer) ? around : null +} + +function findWrappingInside(range, type) { + var parent = range.parent; + var startIndex = range.startIndex; + var endIndex = range.endIndex; + var inner = parent.child(startIndex); + var inside = type.contentMatch.findWrapping(inner.type); + if (!inside) { return null } + var lastType = inside.length ? inside[inside.length - 1] : type; + var innerMatch = lastType.contentMatch; + for (var i = startIndex; innerMatch && i < endIndex; i++) { innerMatch = innerMatch.matchType(parent.child(i).type); } + if (!innerMatch || !innerMatch.validEnd) { return null } + return inside +} +function findWrapping(range, nodeType, attrs, innerRange, customWithAttrs = null) { + if (innerRange === void 0) innerRange = range; + let withAttrs = (type) => ({ type: type, attrs: null }); + var around = findWrappingOutside(range, nodeType); + var inner = around && findWrappingInside(innerRange, nodeType); + if (!inner) { return null } + return around.map(withAttrs).concat({ type: nodeType, attrs: attrs }).concat(inner.map(customWithAttrs ? customWithAttrs : withAttrs)) +} +function wrappingInputRule(regexp, nodeType, getAttrs, joinPredicate, customWithAttrs = null) { + return new prosemirrorInputRules.InputRule(regexp, function (state, match, start, end) { + var attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs; + var tr = state.tr.delete(start, end); + var $start = tr.doc.resolve(start), range = $start.blockRange(), wrapping = range && findWrapping(range, nodeType, attrs, undefined, customWithAttrs); + if (!wrapping) { return null } + tr.wrap(range, wrapping); + var before = tr.doc.resolve(start - 1).nodeBefore; + if (before && before.type == nodeType && prosemirrorTransform.canJoin(tr.doc, start - 1) && + (!joinPredicate || joinPredicate(match, before))) { tr.join(start - 1); } + return tr + }) }
\ No newline at end of file diff --git a/src/client/util/toCSSLineSpacing.js b/src/client/util/toCSSLineSpacing.js new file mode 100644 index 000000000..939d11a0e --- /dev/null +++ b/src/client/util/toCSSLineSpacing.js @@ -0,0 +1,64 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = toCSSLineSpacing; + + +// Line spacing names and their values. +var LINE_SPACING_100 = exports.LINE_SPACING_100 = '125%'; +var LINE_SPACING_115 = exports.LINE_SPACING_115 = '138%'; +var LINE_SPACING_150 = exports.LINE_SPACING_150 = '165%'; +var LINE_SPACING_200 = exports.LINE_SPACING_200 = '232%'; + +var SINGLE_LINE_SPACING = exports.SINGLE_LINE_SPACING = LINE_SPACING_100; +var DOUBLE_LINE_SPACING = exports.DOUBLE_LINE_SPACING = LINE_SPACING_200; + +var NUMBER_VALUE_PATTERN = /^\d+(.\d+)?$/; + +// Normalize the css line-height vlaue to percentage-based value if applicable. +// Also, it calibrates the incorrect line spacing value exported from Google +// Doc. +function toCSSLineSpacing(source) { + if (!source) { + return ''; + } + + var strValue = String(source); + + // e.g. line-height: 1.5; + if (NUMBER_VALUE_PATTERN.test(strValue)) { + var numValue = parseFloat(strValue); + strValue = String(Math.round(numValue * 100)) + '%'; + } + + // Google Doc exports line spacing with wrong values. For instance: + // - Single => 100% + // - 1.15 => 115% + // - Double => 200% + // But the actual CSS value measured in Google Doc is like this: + // - Single => 125% + // - 1.15 => 138% + // - Double => 232% + // The following `if` block will calibrate the value if applicable. + + if (strValue === '100%') { + return LINE_SPACING_100; + } + + if (strValue === '115%') { + return LINE_SPACING_115; + } + + if (strValue === '150%') { + return LINE_SPACING_150; + } + + if (strValue === '200%') { + return LINE_SPACING_200; + } + + // e.g. line-height: 15px; + return strValue; +}
\ No newline at end of file diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 760736501..68b97f2b6 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -246,10 +246,11 @@ export class ContextMenu extends React.Component { this.selectedIndex--; } e.preventDefault(); - } else if (e.key === "Enter") { + } else if (e.key === "Enter" || e.key === "Tab") { const item = this.flatItems[this.selectedIndex]; - item.event(); + item && item.event(); this.closeMenu(); + e.preventDefault(); } } diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 90f7be33f..5f673b3f3 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -10,7 +10,7 @@ library.add(faAngleRight); export interface OriginalMenuProps { description: string; - event: () => void; + event: (stuff?: any) => void; undoable?: boolean; icon: IconProp; //maybe should be optional (icon?) closeMenu?: () => void; @@ -44,7 +44,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select if (this.props.undoable !== false) { batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`); } - await this.props.event(); + await this.props.event({ x: e.clientX, y: e.clientY }); batch && batch.end(); } } @@ -89,17 +89,17 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select ); } else if ("subitems" in this.props) { let submenu = !this.overItem ? (null) : - <div className="contextMenu-subMenu-cont" style={{ marginLeft: "100.5%", left: "0px" }}> + <div className="contextMenu-subMenu-cont" style={{ marginLeft: "25%", left: "0px" }}> {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} </div>; return ( - <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onMouseEnter={this.onPointerEnter} onMouseLeave={this.onPointerLeave}> + <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onMouseLeave={this.onPointerLeave}> {this.props.icon ? ( <span className="icon-background"> <FontAwesomeIcon icon={this.props.icon} size="sm" /> </span> ) : null} - <div className="contextMenu-description"> + <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} > {this.props.description} <FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "10px" }} /> </div> diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 35c45b03c..97876204e 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -30,6 +30,7 @@ import { MetadataEntryMenu } from './MetadataEntryMenu'; import { ImageBox } from './nodes/ImageBox'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; +import { ObjectField } from '../../new_fields/ObjectField'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -147,13 +148,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let fieldTemplateView = SelectionManager.SelectedDocuments()[0]; SelectionManager.DeselectAll(); let fieldTemplate = fieldTemplateView.props.Document; - let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document; - let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length); - let proto = Doc.GetProto(docTemplate); - Doc.MakeTemplate(fieldTemplate, metaKey, proto); - if (text.startsWith(">>")) { - proto.detailedLayout = proto.layout; - proto.miniLayout = ImageBox.LayoutString(metaKey); + let containerView = fieldTemplateView.props.ContainingCollectionView; + if (containerView) { + let docTemplate = containerView.props.Document; + let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length); + let proto = Doc.GetProto(docTemplate); + if (metaKey !== containerView.props.fieldKey && containerView.props.DataDoc) { + const fd = fieldTemplate.data; + fd instanceof ObjectField && (Doc.GetProto(containerView.props.DataDoc)[metaKey] = ObjectField.MakeCopy(fd)); + } + Doc.MakeTemplate(fieldTemplate, metaKey, proto); + if (text.startsWith(">>")) { + proto.detailedLayout = proto.layout; + proto.miniLayout = ImageBox.LayoutString(metaKey); + } } } else { @@ -285,7 +293,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> onCloseUp = async (e: PointerEvent) => { e.stopPropagation(); if (e.button === 0) { - const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc); + const recent = Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc; SelectionManager.SelectedDocuments().map(dv => { recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true); dv.props.removeDocument && dv.props.removeDocument(dv.props.Document); @@ -399,8 +407,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } moveIconDoc(iconDoc: Doc) { let selView = SelectionManager.SelectedDocuments()[0]; - let zoom = NumCast(selView.props.Document.zoomBasis, 1); - let where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()).scale(1 / zoom). + let where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()). transformPoint(this._minimizedX - 12, this._minimizedY - 12); iconDoc.x = where[0] + NumCast(selView.props.Document.x); iconDoc.y = where[1] + NumCast(selView.props.Document.y); @@ -430,6 +437,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1])); SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)). map(d => d.borderRounding = `${Math.min(100, dist)}%`); + SelectionManager.SelectedDocuments().map(dv => { + let cv = dv.props.ContainingCollectionView; + let ruleProvider = cv && (Cast(cv.props.Document.ruleProvider, Doc) as Doc); + let heading = NumCast(dv.props.Document.heading); + cv && ((ruleProvider ? ruleProvider : cv.props.Document)["ruleRounding_" + heading] = StrCast(dv.props.Document.borderRounding)); + }) e.stopPropagation(); e.preventDefault(); } @@ -617,6 +630,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> doc.y = (doc.y || 0) + dY * (actualdH - height); let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here... let fixedAspect = e.ctrlKey || (!BoolCast(doc.ignoreAspect) && nwidth && nheight); + if (fixedAspect && e.ctrlKey && BoolCast(doc.ignoreAspect)) { + doc.ignoreAspect = false; + proto.nativeWidth = nwidth = doc.width || 0; + proto.nativeHeight = nheight = doc.height || 0; + } if (fixedAspect && (!nwidth || !nheight)) { proto.nativeWidth = nwidth = doc.width || 0; proto.nativeHeight = nheight = doc.height || 0; diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index dd5395802..e9db4b048 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -28,7 +28,8 @@ export interface EditableProps { contents: any; fontStyle?: string; fontSize?: number; - height?: number; + height?: number | "auto"; + maxHeight?: number; display?: string; autosuggestProps?: { resetValue: () => void; @@ -145,7 +146,7 @@ export class EditableView extends React.Component<EditableProps> { if (this.props.autosuggestProps) this.props.autosuggestProps.resetValue(); return ( <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} - style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }} + style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} onClick={this.onClick}> <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents}</span> </div> diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 3f40642b5..147418400 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -9,9 +9,11 @@ import { SelectionManager } from "../util/SelectionManager"; import { InkTool } from "../../new_fields/InkField"; import { Doc } from "../../new_fields/Doc"; import { undoBatch, UndoManager } from "../util/UndoManager"; -import { StrCast } from "../../new_fields/Types"; -import { FormattedTextBox } from "./nodes/FormattedTextBox"; +import { StrCast, NumCast, Cast } from "../../new_fields/Types"; import { MainOverlayTextBox } from "./MainOverlayTextBox"; +import { listSpec } from "../../new_fields/Schema"; +import { List } from "../../new_fields/List"; +import { Utils } from "../../Utils"; library.add(faPen, faHighlighter, faEraser, faBan); @@ -49,7 +51,36 @@ export class InkingControl extends React.Component { let oldColors = selected.map(view => { let targetDoc = view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document); let oldColor = StrCast(targetDoc.backgroundColor); - targetDoc.backgroundColor = this._selectedColor; + if (view.props.ContainingCollectionView && view.props.ContainingCollectionView.props.Document.colorPalette) { + let cp = Cast(view.props.ContainingCollectionView.props.Document.colorPalette, listSpec("string")) as string[]; + let closest = 0; + let dist = 10000000; + let ccol = Utils.fromRGBAstr(StrCast(targetDoc.backgroundColor)); + for (let i = 0; i < cp.length; i++) { + let cpcol = Utils.fromRGBAstr(cp[i]); + let d = Math.sqrt((ccol.r - cpcol.r) * (ccol.r - cpcol.r) + (ccol.b - cpcol.b) * (ccol.b - cpcol.b) + (ccol.g - cpcol.g) * (ccol.g - cpcol.g)); + if (d < dist) { + dist = d; + closest = i; + } + } + cp[closest] = "rgba(" + color.rgb.r + "," + color.rgb.g + "," + color.rgb.b + "," + color.rgb.a + ")"; + view.props.ContainingCollectionView.props.Document.colorPalette = new List(cp); + targetDoc.backgroundColor = cp[closest]; + } else { + targetDoc.backgroundColor = this._selectedColor; + } + if (view.props.Document.heading) { + let cv = view.props.ContainingCollectionView; + let ruleProvider = cv && (Cast(cv.props.Document.ruleProvider, Doc) as Doc); + let parback = cv && StrCast(cv.props.Document.backgroundColor); + cv && parback && ((ruleProvider ? ruleProvider : cv.props.Document)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb)); + // if (parback && cv && parback.indexOf("rgb") !== -1) { + // let parcol = Utils.fromRGBAstr(parback); + // let hsl = Utils.RGBToHSL(parcol.r, parcol.g, parcol.b); + // cv && ((ruleProvider ? ruleProvider : cv.props.Document)["ruleColor_" + NumCast(view.props.Document.heading)] = color.hsl.s - hsl.s); + // } + } return { target: targetDoc, previous: oldColor @@ -62,7 +93,6 @@ export class InkingControl extends React.Component { }); } }); - @action switchWidth = (width: string): void => { this._selectedWidth = width; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 271326f70..e35ba18e4 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -57,7 +57,6 @@ let swapDocs = async () => { (await Cast(CurrentUserUtils.UserDocument.workspaces, Doc))!.chromeStatus = "disabled"; (await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))!.chromeStatus = "disabled"; (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled"; - CurrentUserUtils.UserDocument.chromeStatus = "disabled"; await swapDocs(); document.getElementById('root')!.addEventListener('wheel', event => { if (event.ctrlKey) { diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index 27e0d181f..c3a2cb214 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -25,7 +25,6 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> private _textTargetDiv: HTMLDivElement | undefined; private _textProxyDiv: React.RefObject<HTMLDivElement>; private _textBottom: boolean | undefined; - private _textAutoHeight: boolean | undefined; private _setouterdiv = (outerdiv: HTMLElement | null) => { this._outerdiv = outerdiv; this.updateTooltip(); }; private _outerdiv: HTMLElement | null = null; private _textBox: FormattedTextBox | undefined; diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 54ab8696e..da4b71e5c 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -191,13 +191,15 @@ export class OverlayView extends React.Component { zoomToScale={emptyFunction} getScale={returnOne} /> </div>; - }) + }); } render() { return ( <div className="overlayView" id="overlayView"> - {this._elements} + <div> + {this._elements} + </div> {this.overlayDocs} </div> ); diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 1329dc02c..1aed51e64 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -93,9 +93,7 @@ export class PreviewCursor extends React.Component<{}> { @action onKeyPress = (e: KeyboardEvent) => { - // Mixing events between React and Native is finicky. In FormattedTextBox, we set the - // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore - // the keyPress here. 112- + // Mixing events between React and Native is finicky. //if not these keys, make a textbox if preview cursor is active! if (e.key !== "Escape" && e.key !== "Backspace" && e.key !== "Delete" && e.key !== "CapsLock" && e.key !== "Alt" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Control" && @@ -103,8 +101,8 @@ export class PreviewCursor extends React.Component<{}> { e.key !== "NumLock" && (e.keyCode < 112 || e.keyCode > 123) && // F1 thru F12 keys !e.key.startsWith("Arrow") && - !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { - if (!e.ctrlKey && !e.metaKey) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) { + !e.defaultPrevented) { + if ((!e.ctrlKey || (e.keyCode >= 48 && e.keyCode <= 57)) && !e.metaKey) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) { PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e); PreviewCursor.Visible = false; } diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx index 2b862a81e..8f06cf770 100644 --- a/src/client/views/ScriptBox.tsx +++ b/src/client/views/ScriptBox.tsx @@ -10,12 +10,15 @@ import { emptyFunction } from "../../Utils"; import { ScriptCast } from "../../new_fields/Types"; import { CompileScript } from "../util/Scripting"; import { ScriptField } from "../../new_fields/ScriptField"; +import { DragManager } from "../util/DragManager"; +import { EditableView } from "./EditableView"; export interface ScriptBoxProps { onSave: (text: string, onError: (error: string) => void) => void; onCancel?: () => void; initialText?: string; showDocumentIcons?: boolean; + setParams?: (p: string[]) => void; } @observer @@ -56,23 +59,49 @@ export class ScriptBox extends React.Component<ScriptBoxProps> { onFocus = this.onFocus; onBlur = this.onBlur; } + let params = <EditableView + contents={""} + display={"block"} + maxHeight={72} + height={35} + fontSize={28} + GetValue={() => ""} + SetValue={(value: string) => this.props.setParams && this.props.setParams(value.split(" ").filter(s => s !== " ")) ? true : true} + />; return ( <div className="scriptBox-outerDiv"> + <div style={{ display: "flex", flexDirection: "column", height: "100%" }}> + <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText} onFocus={onFocus} onBlur={onBlur}></textarea> + <div style={{ background: "beige" }} >{params}</div> + </div> <div className="scriptBox-toolbar"> <button onClick={e => { this.props.onSave(this._scriptText, this.onError); e.stopPropagation(); }}>Save</button> <button onClick={e => { this.props.onCancel && this.props.onCancel(); e.stopPropagation(); }}>Cancel</button> </div> - <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText} onFocus={onFocus} onBlur={onBlur}></textarea> </div> ); } - public static EditClickScript(doc: Doc, fieldKey: string) { + //let l = docList(this.source[0].data).length; if (l) { let ind = this.target[0].index !== undefined ? (this.target[0].index+1) % l : 0; this.target[0].index = ind; this.target[0].proto = getProto(docList(this.source[0].data)[ind]);} + public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, prewrapper?: string, postwrapper?: string) { let overlayDisposer: () => void = emptyFunction; const script = ScriptCast(doc[fieldKey]); let originalText: string | undefined = undefined; - if (script) originalText = script.script.originalScript; + if (script) { + originalText = script.script.originalScript; + if (prewrapper && originalText.startsWith(prewrapper)) { + originalText = originalText.substr(prewrapper.length); + } + if (postwrapper && originalText.endsWith(postwrapper)) { + originalText = originalText.substr(0, originalText.length - postwrapper.length); + } + } // tslint:disable-next-line: no-unnecessary-callback-wrapper - let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { + let params: string[] = []; + let setParams = (p: string[]) => params.splice(0, params.length, ...p); + let scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={() => overlayDisposer()} onSave={(text, onError) => { + if (prewrapper) { + text = prewrapper + text + (postwrapper ? postwrapper : ""); + } const script = CompileScript(text, { params: { this: Doc.name }, typecheck: false, @@ -83,9 +112,12 @@ export class ScriptBox extends React.Component<ScriptBoxProps> { onError(script.errors.map(error => error.messageText).join("\n")); return; } + + params.length && DragManager.StartButtonDrag([], text, "a script", {}, params, (button: Doc) => { }, clientX, clientY); + doc[fieldKey] = new ScriptField(script); overlayDisposer(); }} showDocumentIcons />; - overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${doc.title || ""} OnClick` }); + overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: title }); } } diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index b6ed6aaa0..b7036b3ff 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -12,6 +12,7 @@ import { ContextMenu } from '../ContextMenu'; import { FieldViewProps } from '../nodes/FieldView'; import './CollectionBaseView.scss'; import { DateField } from '../../../new_fields/DateField'; +import { DocumentType } from '../../documents/DocumentTypes'; export enum CollectionViewType { Invalid, @@ -126,7 +127,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; let value = Cast(targetDataDoc[targetField], listSpec(Doc), []); - let index = value.reduce((p, v, i) => (v instanceof Doc && v[Id] === doc[Id]) ? i : p, -1); + let index = value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined)); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 95f94875c..fb8b0c41b 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -607,7 +607,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } return Transform.Identity(); } - get previewPanelCenteringOffset() { return this.nativeWidth && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 2 : 0; } + get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 2 : 0; } addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => { if (doc.dockingConfig) { diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 9c26a08f0..c59107b53 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -214,7 +214,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> { isEditingCallback={this.isEditingCallback} display={"inline"} contents={contents} - height={Number(MAX_ROW_HEIGHT)} + height={"auto"} + maxHeight={Number(MAX_ROW_HEIGHT)} GetValue={() => { let field = props.Document[props.fieldKey]; if (Field.IsField(field)) { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 97b31bf2a..91e10b0ac 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -310,6 +310,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { sectionMasonry(heading: SchemaHeaderField | undefined, docList: Doc[]) { let cols = Math.max(1, Math.min(docList.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); + if (isNaN(cols)) { + console.log("naN"); + cols = 1; + } return <div key={heading ? heading.heading : "empty"} className="collectionStackingView-masonrySection"> {!heading ? (null) : <div key={`${heading.heading}`} className="collectionStackingView-sectionHeader" style={{ background: heading.color }} @@ -358,8 +362,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { subItems.push({ description: `${this.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" }); subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" }); subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" }); - subItems.push({ description: "Edit onChildClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onChildClick") }); ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" }); + + let existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 50f03005c..f5bb76966 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -52,6 +52,7 @@ export interface TreeViewProps { active: () => boolean; showHeaderFields: () => boolean; preventTreeViewOpen: boolean; + renderedIds: string[]; } library.add(faTrashAlt); @@ -274,7 +275,9 @@ class TreeView extends React.Component<TreeViewProps> { let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : DocListCast(contents), this.props.treeViewId, doc, undefined, key, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen); + this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, + this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen, + [...this.props.renderedIds, doc[Id]]); } else { contentElement = <EditableView key="editableView" @@ -306,7 +309,8 @@ class TreeView extends React.Component<TreeViewProps> { TreeView.GetChildElements(docs as Doc[], this.props.treeViewId, this.props.document.layout as Doc, this.resolvedDataDoc, expandKey, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, - this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen)} + this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen, + [...this.props.renderedIds, this.props.document[Id]])} </ul >; } else if (this.treeViewExpandedView === "fields") { return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}> @@ -373,6 +377,7 @@ class TreeView extends React.Component<TreeViewProps> { style={{ color: this.props.document.isMinimized ? "red" : "black", background: Doc.IsBrushed(this.props.document) ? "#06121212" : "0", + fontWeight: this.props.document.search_string ? "bold" : undefined, outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" }} > @@ -391,7 +396,7 @@ class TreeView extends React.Component<TreeViewProps> { {this.renderTitle} </div> <div className="treeViewItem-border"> - {!this.treeViewOpen ? (null) : this.renderContent} + {!this.treeViewOpen || this.props.renderedIds.indexOf(this.props.document[Id]) !== -1 ? (null) : this.renderContent} </div> </li> </div>; @@ -414,7 +419,8 @@ class TreeView extends React.Component<TreeViewProps> { panelWidth: () => number, renderDepth: number, showHeaderFields: () => boolean, - preventTreeViewOpen: boolean + preventTreeViewOpen: boolean, + renderedIds: string[] ) { let docs = docList.filter(child => !child.excludeFromLibrary && child.opacity !== 0); let viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); @@ -501,7 +507,8 @@ class TreeView extends React.Component<TreeViewProps> { parentKey={key} active={active} showHeaderFields={showHeaderFields} - preventTreeViewOpen={preventTreeViewOpen} />; + preventTreeViewOpen={preventTreeViewOpen} + renderedIds={renderedIds} />; }); } } @@ -591,13 +598,14 @@ export class CollectionTreeView extends CollectionSubView(Document) { <div id="body" className="collectionTreeView-dropTarget" style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray") }} onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()} + onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> <EditableView contents={this.resolvedDataDoc.title} display={"block"} - height={72} + maxHeight={72} + height={"auto"} GetValue={() => StrCast(this.resolvedDataDoc.title)} SetValue={undoBatch((value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true)} OnFillDown={undoBatch((value: string) => { @@ -614,7 +622,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove, moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth, () => this.props.Document.chromeStatus !== "disabled", - BoolCast(this.props.Document.preventTreeViewOpen)) + BoolCast(this.props.Document.preventTreeViewOpen), []) } </ul> </div > diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 17111af58..d8475a467 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -38,8 +38,8 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { return () => { col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { - const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2; - const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2; + const newPanX = NumCast(target.x) + NumCast(target.width) / 2; + const newPanY = NumCast(target.y) + NumCast(target.height) / 2; col.panX = newPanX; col.panY = newPanY; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 6af87b138..790c6694b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -39,10 +39,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo // let l = this.props.LinkDocs; let a = this.props.A; let b = this.props.B; - let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2); - let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2); - let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2); - let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2); + let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / 2); + let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2); + let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2); + let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2); let text = ""; // let first = this.props.LinkDocs[0]; // if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : ""); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 2d94f1b8e..a25627dd1 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -31,8 +31,8 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP // let srcTarg = srcDoc; // let x1 = NumCast(srcDoc.x); // let x2 = NumCast(dstDoc.x); - // let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1); - // let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1); + // let x1w = NumCast(srcDoc.width, -1); + // let x2w = NumCast(dstDoc.width, -1); // if (x1w < 0 || x2w < 0 || i === j) { } // else { // let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => { @@ -120,9 +120,9 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP render() { return ( <div className="collectionfreeformlinksview-container"> - {/* <svg className="collectionfreeformlinksview-svgCanvas"> + <svg className="collectionfreeformlinksview-svgCanvas"> {this.uniqueConnections} - </svg> */} + </svg> {this.props.children} </div> ); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 1af534ecd..3377775db 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -3,12 +3,12 @@ import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; import { action, computed, IReactionDisposer, observable, reaction, trace } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCastAsync, Field, FieldResult, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc"; +import { Doc, DocListCastAsync, Field, FieldResult, HeightSym, Opt, WidthSym, DocListCast } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types"; +import { BoolCast, Cast, FieldValue, NumCast, StrCast, PromiseValue } from "../../../../new_fields/Types"; import { emptyFunction, returnEmptyString, returnOne, Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { Docs } from "../../../documents/Documents"; @@ -39,6 +39,7 @@ import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { DocServer } from "../../../DocServer"; import { FormattedTextBox } from "../../nodes/FormattedTextBox"; +import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard); @@ -153,6 +154,7 @@ export namespace PivotView { y={pos.y} width={pos.width} height={pos.height} + jitterRotation={NumCast(target.props.Document.jitterRotation)} {...target.getChildDocumentViewProps(doc)} />, bounds: { @@ -225,8 +227,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return bounds; } + @computed get actualContentBounds() { + return this.fitToBox && !this.isAnnotationOverlay ? this.ComputeContentBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)) : undefined; + } + @computed get contentBounds() { - let bounds = this.fitToBox && !this.isAnnotationOverlay ? this.ComputeContentBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)) : undefined; + let bounds = this.actualContentBounds; let res = { panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0, panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0, @@ -252,12 +258,34 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); private addLiveTextBox = (newBox: Doc) => { FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed - this.addDocument(newBox, false); + newBox.heading = 1; + for (let i = 0; i < this.childDocs.length; i++) { + if (this.childDocs[i].heading == 1) { + newBox.heading = 2; + } + } + PromiseValue(Cast(this.props.Document.ruleProvider, Doc)).then(ruleProvider => { + if (!ruleProvider) ruleProvider = this.props.Document; + // saturation shift + // let col = NumCast(ruleProvider["ruleColor_" + NumCast(newBox.heading)]); + // let back = Utils.fromRGBAstr(StrCast(this.props.Document.backgroundColor)); + // let hsl = Utils.RGBToHSL(back.r, back.g, back.b); + // let newcol = { h: hsl.h, s: hsl.s + col, l: hsl.l }; + // col && (Doc.GetProto(newBox).backgroundColor = Utils.toRGBAstr(Utils.HSLtoRGB(newcol.h, newcol.s, newcol.l))); + // OR transparency set + let col = StrCast(ruleProvider["ruleColor_" + NumCast(newBox.heading)]); + (newBox.backgroundColor === newBox.defaultBackgroundColor) && col && (Doc.GetProto(newBox).backgroundColor = col); + + let round = StrCast(ruleProvider["ruleRounding_" + NumCast(newBox.heading)]); + round && (Doc.GetProto(newBox).borderRounding = round); + newBox.ruleProvider = ruleProvider; + this.addDocument(newBox, false); + }); } private addDocument = (newBox: Doc, allowDuplicates: boolean) => { this.props.addDocument(newBox, false); this.bringToFront(newBox); - this.updateClusters(); + this.updateCluster(newBox); return true; } private selectDocuments = (docs: Doc[]) => { @@ -325,7 +353,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.bringToFront(d); }); - this.updateClusters(); + de.data.droppedDocuments.length == 1 && this.updateCluster(de.data.droppedDocuments[0]); } } else if (de.data instanceof DragManager.AnnotationDragData) { @@ -385,6 +413,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return false; } @observable sets: (Doc[])[] = []; + + @undoBatch @action updateClusters() { this.sets.length = 0; @@ -413,12 +443,42 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.sets.map((set, i) => set.map(member => member.cluster = i)); } + @undoBatch + @action + updateCluster(doc: Doc) { + if (this.props.Document.useClusters) { + this.sets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); + let preferredInd = NumCast(doc.cluster); + doc.cluster = -1; + this.sets.map((set, i) => set.map(member => { + if (doc.cluster === -1 && Doc.IndexOf(member, this.childDocs) !== -1 && this.boundsOverlap(doc, member)) { + doc.cluster = i; + } + })); + if (doc.cluster === -1 && preferredInd !== -1 && (!this.sets[preferredInd] || !this.sets[preferredInd].filter(member => Doc.IndexOf(member, this.childDocs) !== -1).length)) { + doc.cluster = preferredInd; + } + this.sets.map((set, i) => { + if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, this.childDocs) !== -1).length) { + doc.cluster = i; + } + }); + if (doc.cluster === -1) { + doc.cluster = this.sets.length; + this.sets.push([doc]); + } else { + for (let i = this.sets.length; i <= doc.cluster; i++) !this.sets[i] && this.sets.push([]); + this.sets[doc.cluster].push(doc); + } + } + } + getClusterColor = (doc: Doc) => { if (this.props.Document.useClusters) { let cluster = NumCast(doc.cluster); if (this.sets.length <= cluster) { - setTimeout(() => this.updateClusters(), 0); - return; + setTimeout(() => this.updateCluster(doc), 0);// this.updateClusters(), 0); + return ""; } let set = this.sets.length > cluster ? this.sets[cluster] : undefined; let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"]; @@ -743,7 +803,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const initScript = this.Document.arrangeInit; const script = this.Document.arrangeScript; let state: any = undefined; - const docs = this.childDocs; + let docs = this.childDocs; + let overlayDocs = DocListCast(this.props.Document.localOverlays); + overlayDocs && docs.push(...overlayDocs); let elements: ViewDefResult[] = []; if (initScript) { const initResult = initScript.script.run({ docs, collection: this.Document }); @@ -766,6 +828,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (pair.layout && !(pair.data instanceof Promise)) { prev.push({ ele: <CollectionFreeFormDocumentView key={doc[Id]} + jitterRotation={NumCast(this.props.Document.jitterRotation)} x={script ? pos.x : undefined} y={script ? pos.y : undefined} width={script ? pos.width : undefined} height={script ? pos.height : undefined} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />, bounds: { x: pos.x || 0, y: pos.y || 0, z: pos.z, width: NumCast(pos.width), height: NumCast(pos.height) } @@ -870,6 +933,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { Docs.Prototypes.get(DocumentType.TEXT).defaultBackgroundColor = "#f1efeb"; // backward compatibility with databases that didn't have a default background color on prototypes Docs.Prototypes.get(DocumentType.COL).defaultBackgroundColor = "white"; this.props.Document.useClusters = !this.props.Document.useClusters; + this.updateClusters(); }, icon: !this.props.Document.useClusters ? "braille" : "braille" }); @@ -880,9 +944,22 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }); layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" }); layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); + layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" }); + + let noteItems: ContextMenuProps[] = []; + let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); + notes.map((node, i) => noteItems.push({ description: (i + 1) + ": " + StrCast(node.title), event: () => this.createText(i), icon: "eye" })); + layoutItems.push({ description: "Add Note ...", subitems: noteItems, icon: "eye" }) ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" }); } + createText = (noteStyle: number) => { + let pt = this.getTransform().transformPoint(ContextMenu.Instance.pageX, ContextMenu.Instance.pageY); + let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); + let text = Docs.Create.TextDocument({ width: 200, height: 100, x: pt[0], y: pt[1], autoHeight: true, title: StrCast(notes[noteStyle % notes.length].title) }); + text.layout = notes[noteStyle % notes.length]; + this.addLiveTextBox(text); + } private childViews = () => [ <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />, @@ -921,6 +998,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } render() { + this.props.Document.fitX = this.actualContentBounds && this.actualContentBounds.x; + this.props.Document.fitY = this.actualContentBounds && this.actualContentBounds.y; + this.props.Document.fitW = this.actualContentBounds && (this.actualContentBounds.r - this.actualContentBounds.x); + this.props.Document.fitH = this.actualContentBounds && (this.actualContentBounds.b - this.actualContentBounds.y); const easing = () => this.props.Document.panTransformType === "Ease"; Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); return ( diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 27eafd769..fe48a3485 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,11 +1,11 @@ import * as htmlToImage from "html-to-image"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, FieldResult } from "../../../../new_fields/Doc"; +import { Doc, FieldResult, DocListCast } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; -import { Cast, NumCast } from "../../../../new_fields/Types"; +import { Cast, NumCast, StrCast } from "../../../../new_fields/Types"; import { Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; @@ -20,6 +20,9 @@ import { CollectionFreeFormView } from "./CollectionFreeFormView"; import "./MarqueeView.scss"; import React = require("react"); import { SchemaHeaderField, RandomPastel } from "../../../../new_fields/SchemaHeaderField"; +import { string } from "prop-types"; +import { listSpec } from "../../../../new_fields/Schema"; +import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -93,9 +96,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } }); } else if (!e.ctrlKey) { - let newBox = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); - newBox.proto!.autoHeight = true; - this.props.addLiveTextDocument(newBox); + this.props.addLiveTextDocument( + Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" })); + } else if (e.keyCode > 48 && e.keyCode <= 57) { + let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); + let text = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }); + text.layout = notes[(e.keyCode - 49) % notes.length]; + this.props.addLiveTextDocument(text); } e.stopPropagation(); } @@ -273,14 +280,33 @@ export class MarqueeView extends React.Component<MarqueeViewProps> return d; }); } + let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)", + "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",]; + let colorPalette = Cast(this.props.container.props.Document.colorPalette, listSpec("string")); + if (!colorPalette) this.props.container.props.Document.colorPalette = new List<string>(defaultPalette); + let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]); + let usedPaletted = new Map<string, number>(); + [...this.props.activeDocuments(), this.props.container.props.Document].map(child => { + let bg = StrCast(child.backgroundColor); + if (palette.indexOf(bg) !== -1) { + palette.splice(palette.indexOf(bg), 1); + if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1); + else usedPaletted.set(bg, 1); + } + }); + usedPaletted.delete("#f1efeb"); + usedPaletted.delete("white"); + usedPaletted.delete("rgba(255,255,255,1)"); + let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0); + let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0]; let inkData = this.ink ? this.ink.inkData : undefined; let newCollection = Docs.Create.FreeformDocument(selected, { x: bounds.left, y: bounds.top, panX: 0, panY: 0, - backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white", - defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white", + backgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor, + defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor, width: bounds.width, height: bounds.height, title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection", @@ -330,6 +356,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.props.addLiveTextDocument(summary); } else { + newCollection.ruleProvider = this.props.container.props.Document; this.props.addDocument(newCollection, false); this.props.selectDocuments([newCollection]); } diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx index 74663f9af..f8807641b 100644 --- a/src/client/views/linking/LinkFollowBox.tsx +++ b/src/client/views/linking/LinkFollowBox.tsx @@ -17,7 +17,7 @@ import { listSpec } from "../../../new_fields/Schema"; import { DocServer } from "../../DocServer"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faTimes } from '@fortawesome/free-solid-svg-icons'; -import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; +import { docs_v1 } from "googleapis"; enum FollowModes { OPENTAB = "Open in Tab", @@ -180,8 +180,8 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { openColFullScreen = (options: { context: Doc }) => { if (LinkFollowBox.destinationDoc) { if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { - const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / NumCast(LinkFollowBox.destinationDoc.zoomBasis, 1) / 2; - const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / NumCast(LinkFollowBox.destinationDoc.zoomBasis, 1) / 2; + const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2; + const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2; options.context.panX = newPanX; options.context.panY = newPanY; } @@ -198,13 +198,19 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { } + _addDocTab: (undefined | ((doc: Doc, dataDoc: Doc | undefined, where: string) => void)); + + setAddDocTab = (addFunc: (doc: Doc, dataDoc: Doc | undefined, where: string) => void) => { + this._addDocTab = addFunc; + } + @undoBatch openLinkColRight = (options: { context: Doc, shouldZoom: boolean }) => { if (LinkFollowBox.destinationDoc) { options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context; if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { - const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / NumCast(LinkFollowBox.destinationDoc.zoomBasis, 1) / 2; - const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / NumCast(LinkFollowBox.destinationDoc.zoomBasis, 1) / 2; + const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2; + const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2; options.context.panX = newPanX; options.context.panY = newPanY; } @@ -239,22 +245,23 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { let proto = Doc.GetProto(LinkFollowBox.linkDoc); let targetContext = await Cast(proto.targetContext, Doc); let sourceContext = await Cast(proto.sourceContext, Doc); + const shouldZoom = options ? options.shouldZoom : false; - let dockingFunc = (document: Doc) => { this.props.addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); }; + let dockingFunc = (document: Doc) => { this._addDocTab && this._addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); }; if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 && targetContext) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, options.shouldZoom, false, async document => dockingFunc(document), undefined, targetContext); + DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, async document => dockingFunc(document), undefined, targetContext); } else if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor1 && sourceContext) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, options.shouldZoom, false, document => dockingFunc(sourceContext!)); + DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, document => dockingFunc(sourceContext!)); } else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, options.shouldZoom, undefined, undefined, + DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, undefined, undefined, NumCast((LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 ? LinkFollowBox.linkDoc.anchor2Page : LinkFollowBox.linkDoc.anchor1Page))); } else { - DocumentManager.Instance.jumpToDocument(jumpToDoc, options.shouldZoom, false, dockingFunc); + DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, dockingFunc); } this.highlightDoc(); @@ -266,9 +273,8 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { openLinkTab = () => { if (LinkFollowBox.destinationDoc) { let fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc); - // THIS IS EMPTY FUNCTION - this.props.addDocTab(fullScreenAlias, undefined, "inTab"); - console.log(this.props.addDocTab); + // this.prosp.addDocTab is empty -- use the link source's addDocTab + this._addDocTab && this._addDocTab(fullScreenAlias, undefined, "inTab"); this.highlightDoc(); SelectionManager.DeselectAll(); @@ -280,12 +286,12 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { if (LinkFollowBox.destinationDoc) { options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context; if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { - const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / NumCast(LinkFollowBox.destinationDoc.zoomBasis, 1) / 2; - const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / NumCast(LinkFollowBox.destinationDoc.zoomBasis, 1) / 2; + const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2; + const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2; options.context.panX = newPanX; options.context.panY = newPanY; } - this.props.addDocTab(options.context, undefined, "inTab"); + this._addDocTab && this._addDocTab(options.context, undefined, "inTab"); if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom }); this.highlightDoc(); @@ -321,7 +327,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { } //set this to be the default link behavior, can be any of the above - public defaultLinkBehavior: (options?: any) => void = this.openLinkTab; + public defaultLinkBehavior: (options?: any) => void = this.jumpToLink; @action currentLinkBehavior = () => { diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 6895dae9a..19a0023e9 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -1,26 +1,17 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp, faGlobeAsia } from '@fortawesome/free-solid-svg-icons'; +import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, observable } from 'mobx'; import { observer } from "mobx-react"; -import { DocumentManager } from "../../util/DocumentManager"; -import { undoBatch } from "../../util/UndoManager"; -import './LinkMenu.scss'; -import React = require("react"); -import { Doc, DocListCastAsync, WidthSym } from '../../../new_fields/Doc'; -import { StrCast, Cast, FieldValue, NumCast } from '../../../new_fields/Types'; -import { observable, action, computed } from 'mobx'; -import { LinkManager } from '../../util/LinkManager'; +import { Doc } from '../../../new_fields/Doc'; +import { Cast, StrCast } from '../../../new_fields/Types'; import { DragLinkAsDocument } from '../../util/DragManager'; -import { CollectionDockingView } from '../collections/CollectionDockingView'; -import { SelectionManager } from '../../util/SelectionManager'; -import { CollectionViewType } from '../collections/CollectionBaseView'; -import { DocumentView } from '../nodes/DocumentView'; -import { SearchUtil } from '../../util/SearchUtil'; -import { LinkFollowBox } from './LinkFollowBox'; +import { LinkManager } from '../../util/LinkManager'; import { ContextMenu } from '../ContextMenu'; import { MainView } from '../MainView'; -import { Docs } from '../../documents/Documents'; -import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { LinkFollowBox } from './LinkFollowBox'; +import './LinkMenu.scss'; +import React = require("react"); library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp); @@ -37,7 +28,9 @@ interface LinkMenuItemProps { export class LinkMenuItem extends React.Component<LinkMenuItemProps> { private _drag = React.createRef<HTMLDivElement>(); @observable private _showMore: boolean = false; - @action toggleShowMore() { this._showMore = !this._showMore; } + @action toggleShowMore() { + this._showMore = !this._showMore; + } onEdit = (e: React.PointerEvent): void => { e.stopPropagation(); @@ -75,6 +68,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { onLinkButtonUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onLinkButtonMoved); document.removeEventListener("pointerup", this.onLinkButtonUp); + + if (LinkFollowBox.Instance !== undefined) { + LinkFollowBox.Instance.props.Document.isMinimized = false; + LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc); + LinkFollowBox.Instance.setAddDocTab(this.props.addDocTab); + } e.stopPropagation(); } @@ -98,7 +97,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { @action.bound async followDefault() { if (LinkFollowBox.Instance !== undefined) { - LinkFollowBox.Instance.props.Document.isMinimized = false; LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc); LinkFollowBox.Instance.defaultLinkBehavior(); } diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx index 54848344b..68d3b8ae1 100644 --- a/src/client/views/nodes/ButtonBox.tsx +++ b/src/client/views/nodes/ButtonBox.tsx @@ -13,6 +13,8 @@ import { undoBatch } from '../../util/UndoManager'; import { DocComponent } from '../DocComponent'; import './ButtonBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { ContextMenu } from '../ContextMenu'; library.add(faEdit as any); @@ -41,11 +43,24 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); } } + + specificContextMenu = (e: React.MouseEvent): void => { + let funcs: ContextMenuProps[] = []; + funcs.push({ + description: "Clear Script Params", event: () => { + let params = Cast(this.props.Document.buttonParams, listSpec("string")); + params && params.map(p => this.props.Document[p] = undefined) + }, icon: "trash" + }); + + ContextMenu.Instance.addItem({ description: "OnClick...", subitems: funcs, icon: "asterisk" }); + } + @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (de.data instanceof DragManager.DocumentDragData) { - Doc.GetProto(this.dataDoc).source = new List<Doc>(de.data.droppedDocuments); + if (de.data instanceof DragManager.DocumentDragData && e.target) { + this.props.Document[(e.target as any).textContent] = new List<Doc>(de.data.droppedDocuments); e.stopPropagation(); } } @@ -55,7 +70,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt let missingParams = params && params.filter(p => this.props.Document[p] === undefined); params && params.map(async p => await DocListCastAsync(this.props.Document[p])); // bcz: really hacky form of prefetching ... return ( - <div className="buttonBox-outerDiv" ref={this.createDropTarget} > + <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> <div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} > <div className="buttonBox-mainButtonCenter"> {(this.Document.text || this.Document.title)} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index c9c394960..07dd1cae7 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,23 +1,24 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { BoolCast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { BoolCast, FieldValue, NumCast, StrCast, Cast } from "../../../new_fields/Types"; import { Transform } from "../../util/Transform"; import { DocComponent } from "../DocComponent"; import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); import { Doc } from "../../../new_fields/Doc"; +import { random } from "animejs"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { x?: number; y?: number; width?: number; height?: number; + jitterRotation: number; } const schema = createSchema({ - zoomBasis: "number", zIndex: "number", }); @@ -27,22 +28,36 @@ const FreeformDocument = makeInterface(schema, positionSchema); @observer export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) { - @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}) `; } - @computed get X() { return this.props.x !== undefined ? this.props.x : this.Document.x || 0; } - @computed get Y() { return this.props.y !== undefined ? this.props.y : this.Document.y || 0; } - @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.width !== undefined ? this.props.width : this.Document.width || 0; } - @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.height !== undefined ? this.props.height : this.Document.height || 0; } - @computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); } + @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; } + @computed get X() { return this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.Document.x || 0; } + @computed get Y() { return this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.Document.y || 0; } + @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.Document.width || 0; } + @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.Document.height || 0; } @computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); } @computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); } @computed get scaleToOverridingWidth() { return this.width / NumCast(this.props.Document.width, this.width); } + @computed get renderScriptDim() { + if (this.Document.renderScript) { + let someView = Cast(this.Document.someView, Doc); + let minimap = Cast(this.Document.minimap, Doc); + if (someView instanceof Doc && minimap instanceof Doc) { + let x = (NumCast(someView.panX) - NumCast(someView.width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap.width) - NumCast(minimap.width) / 2; + let y = (NumCast(someView.panY) - NumCast(someView.height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap.height) - NumCast(minimap.height) / 2; + let w = NumCast(someView.width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width); + let h = NumCast(someView.height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height); + return { x: x, y: y, width: w, height: h }; + } + } + return undefined; + } + contentScaling = () => this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? this.width / this.nativeWidth : 1; panelWidth = () => this.props.PanelWidth(); panelHeight = () => this.props.PanelHeight(); getTransform = (): Transform => this.props.ScreenToLocalTransform() .translate(-this.X, -this.Y) - .scale(1 / this.contentScaling()).scale(1 / this.zoom / this.scaleToOverridingWidth) + .scale(1 / this.contentScaling()).scale(1 / this.scaleToOverridingWidth) animateBetweenIcon = (icon: number[], stime: number, maximizing: boolean) => { this.props.bringToFront(this.props.Document); @@ -59,10 +74,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } borderRounding = () => { - let br = StrCast(this.props.Document.layout instanceof Doc ? this.props.Document.layout.borderRounding : this.props.Document.borderRounding); + let br = StrCast(this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout.borderRounding : this.props.Document.borderRounding); if (br.endsWith("%")) { let percent = Number(br.substr(0, br.length - 1)) / 100; - let nativeDim = Math.min(NumCast(this.props.Document.nativeWidth), NumCast(this.props.Document.nativeHeight)); + let nativeDim = Math.min(NumCast(this.layoutDoc.nativeWidth), NumCast(this.layoutDoc.nativeHeight)); let minDim = percent * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight())); return minDim; } @@ -74,6 +89,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF clusterColorFunc = (doc: Doc) => this.clusterColor; + get layoutDoc() { + // if this document's layout field contains a document (ie, a rendering template), then we will use that + // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. + return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; + } + render() { const hasPosition = this.props.x !== undefined || this.props.y !== undefined; return ( @@ -82,13 +103,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF transformOrigin: "left top", position: "absolute", backgroundColor: "transparent", - boxShadow: this.props.Document.opacity === 0 ? undefined : this.props.Document.z ? `#9c9396 ${StrCast(this.props.Document.boxShadow, "10px 10px 0.9vw")}` : - this.clusterColor ? ( - this.props.Document.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` : - `${this.clusterColor} ${StrCast(this.props.Document.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) : undefined, + boxShadow: + this.layoutDoc.opacity === 0 ? undefined : // if it's not visible, then no shadow + this.layoutDoc.z ? `#9c9396 ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow + this.layoutDoc.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` : // if it's a background & has a cluster color, make the shadow spread really big + this.clusterColor ? ( + `${this.clusterColor} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent + StrCast(this.layoutDoc.boxShadow, ""), borderRadius: this.borderRounding(), transform: this.transform, - transition: hasPosition ? "transform 1s" : StrCast(this.props.Document.transition), + transition: hasPosition ? "transform 1s" : StrCast(this.layoutDoc.transition), width: this.width, height: this.height, zIndex: this.Document.zIndex || 0, diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 1e4216dbb..2a8030c0c 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -377,8 +377,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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 => { // open up target if it's not already in view ... + let cv = this.props.ContainingCollectionView; // bcz: ugh --- maybe need to have a props.unfocus() method so that we leave things in the state we found them?? + let px = cv && cv.props.Document.panX; + let py = cv && cv.props.Document.panY; + let s = cv && cv.props.Document.scale; this.props.focus(this.props.Document, true, 1); // by zooming into the button document first - setTimeout(() => this.props.addDocTab(document, undefined, maxLocation), 1000); // then after the 1sec animation, open up the target in a new tab + setTimeout(() => { + this.props.addDocTab(document, undefined, maxLocation); + cv && (cv.props.Document.panX = px); + cv && (cv.props.Document.panY = py); + cv && (cv.props.Document.scale = s); + }, 1000); // then after the 1sec animation, open up the target in a new tab }, linkedFwdPage[altKey ? 1 : 0], targetContext); } @@ -438,17 +447,36 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @undoBatch + makeCustomViewClicked = (): void => { + let options = { title: "data", width: NumCast(this.props.Document.width), height: NumCast(this.props.Document.height) + 25, x: -NumCast(this.props.Document.width) / 2, y: -NumCast(this.props.Document.height) / 2, }; + let fieldTemplate = this.props.Document.type === DocumentType.TEXT ? Docs.Create.TextDocument(options) : Docs.Create.ImageDocument("http://www.cs.brown.edu", options); + + let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: StrCast(this.Document.title) + "layout", width: NumCast(this.props.Document.width) + 20, height: Math.max(100, NumCast(this.props.Document.height) + 45) }); + let metaKey = "data"; + let proto = Doc.GetProto(docTemplate); + Doc.MakeTemplate(fieldTemplate, metaKey, proto); + + Doc.ApplyTemplateTo(docTemplate, this.props.Document, undefined, false); + } + + @undoBatch makeBtnClicked = (): void => { let doc = Doc.GetProto(this.props.Document); - doc.isButton = !BoolCast(doc.isButton); - if (doc.isButton) { - if (!doc.nativeWidth) { - doc.nativeWidth = this.props.Document[WidthSym](); - doc.nativeHeight = this.props.Document[HeightSym](); - } + if (doc.isButton || doc.onClick) { + doc.isButton = false; + doc.onClick = undefined; } else { - doc.nativeWidth = doc.nativeHeight = undefined; + doc.isButton = true; } + + // if (doc.isButton) { + // if (!doc.nativeWidth) { + // doc.nativeWidth = this.props.Document[WidthSym](); + // doc.nativeHeight = this.props.Document[HeightSym](); + // } + // } else { + // doc.nativeWidth = doc.nativeHeight = undefined; + // } } @undoBatch @@ -547,15 +575,28 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @undoBatch @action + makeIntoPortal = (): void => { + if (!DocListCast(this.props.Document.links).find(doc => { + if (Cast(doc.anchor2, Doc) instanceof Doc && (Cast(doc.anchor2, Doc) as Doc)!.title === this.props.Document.title + ".portal") return true; + return false; + })) { + let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" }); + DocUtils.MakeLink(this.props.Document, portal, undefined, this.props.Document.title + ".portal"); + Doc.GetProto(this.props.Document).isButton = true; + } + } + + @undoBatch + @action makeBackground = (): void => { - this.props.Document.isBackground = !this.props.Document.isBackground; - this.props.Document.isBackground && this.props.bringToFront(this.props.Document, true); + this.layoutDoc.isBackground = !this.layoutDoc.isBackground; + this.layoutDoc.isBackground && this.props.bringToFront(this.layoutDoc, true); } @undoBatch @action toggleLockPosition = (): void => { - this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true; + this.layoutDoc.lockedPosition = BoolCast(this.layoutDoc.lockedPosition) ? undefined : true; } listen = async () => { @@ -601,24 +642,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let existingMake = ContextMenu.Instance.findByDescription("Make..."); let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Into Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" }); - makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Into Button", event: this.makeBtnClicked, icon: "concierge-bell" }); - makes.push({ description: "OnClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onClick") }); - makes.push({ - description: "Into Portal", event: () => { - let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" }); - DocUtils.MakeLink(this.props.Document, portal, undefined, this.props.Document.title + ".portal"); - this.makeBtnClicked(); - }, icon: "window-restore" - }); - makes.push({ description: this.props.Document.ignoreClick ? "Selectable" : "Unselectable", event: () => this.props.Document.ignoreClick = !this.props.Document.ignoreClick, icon: this.props.Document.ignoreClick ? "unlock" : "lock" }); + makes.push({ description: "Custom Document View", event: this.makeCustomViewClicked, icon: "concierge-bell" }); + makes.push({ description: "Metadata Field View", event: () => this.props.ContainingCollectionView && Doc.MakeTemplate(this.props.Document, StrCast(this.props.Document.title), this.props.ContainingCollectionView.props.Document), icon: "concierge-bell" }); + makes.push({ description: "Into Portal", event: this.makeIntoPortal, icon: "window-restore" }); + makes.push({ description: this.layoutDoc.ignoreClick ? "Selectable" : "Unselectable", event: () => this.layoutDoc.ignoreClick = !this.layoutDoc.ignoreClick, icon: this.layoutDoc.ignoreClick ? "unlock" : "lock" }); !existingMake && cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" }); + + let existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ description: this.props.Document.isButton || this.props.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" }); + onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) }); + onClicks.push({ + description: "Edit onClick Foreach Doc Script", icon: "edit", event: (obj: any) => { + this.props.Document.collectionContext = this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; + ScriptBox.EditButtonScript("Foreach Collection Doc (d) => ", this.props.Document, "onClick", obj.x, obj.y, "docList(this.collectionContext.data).map(d => {", "});\n"); + } + }); + !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); + let existing = ContextMenu.Instance.findByDescription("Layout..."); let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; - - layoutItems.push({ description: `${this.props.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.props.Document.chromeStatus = (this.props.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); - layoutItems.push({ description: `${this.props.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.props.Document.autoHeight = !this.props.Document.autoHeight, icon: "plus" }); - layoutItems.push({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); - layoutItems.push({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); + layoutItems.push({ description: `${this.layoutDoc.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.layoutDoc.chromeStatus = (this.layoutDoc.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); + layoutItems.push({ description: `${this.layoutDoc.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc.autoHeight = !this.layoutDoc.autoHeight, icon: "plus" }); + layoutItems.push({ description: this.props.Document.ignoreAspect || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); + layoutItems.push({ description: this.layoutDoc.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.layoutDoc.lockedPosition) ? "unlock" : "lock" }); layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) { @@ -639,12 +686,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); cm.addItem({ description: "Move To Overlay", icon: "laptop-code", event: () => ((o: Doc) => o && Doc.AddDocToList(o, "data", this.props.Document))(Cast(CurrentUserUtils.UserDocument.overlays, Doc) as Doc) }); cm.addItem({ - description: "Download document", icon: "download", event: () => { - const a = document.createElement("a"); - const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); - a.href = url; - a.download = `DocExport-${this.props.Document[Id]}.zip`; - a.click(); + description: "Download document", icon: "download", event: async () => { + let y = JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), { + qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' } + })); + console.log(y); + // const a = document.createElement("a"); + // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); + // a.href = url; + // a.download = `DocExport-${this.props.Document[Id]}.zip`; + // a.click(); } }); @@ -768,11 +819,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined; - let brushDegree = Doc.IsBrushedDegree(this.props.Document); let fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); - // console.log(fullDegree) let borderRounding = StrCast(Doc.GetProto(this.props.Document).borderRounding); let localScale = this.props.ScreenToLocalTransform().Scale * fullDegree; + let searchHighlight = (!this.props.Document.search_fields ? (null) : + <div key="search" style={{ position: "absolute", background: "yellow", bottom: "-20px", borderRadius: "5px", transformOrigin: "bottom left", width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> + {StrCast(this.props.Document.search_fields)} + </div>); return ( <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} @@ -793,9 +846,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} > - {!showTitle && !showCaption ? this.contents : + {!showTitle && !showCaption ? + this.props.Document.search_fields ? <div> + {this.contents} + {searchHighlight} + </div> : + this.contents : <div style={{ position: "absolute", display: "inline-block", width: "100%", height: "100%", pointerEvents: "none" }}> - <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 29px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "29px" : undefined }}> {this.contents} </div> @@ -821,6 +878,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu <FormattedTextBox {...this.props} onClick={this.onClickHandler} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} /> </div> } + {searchHighlight} </div> } </div> diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 8f47402c4..d7ac7a9c5 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -118,6 +118,9 @@ footnote::after { width: max-content; } +.prosemirror-attribution { + font-size: 8px; +} .footnote-tooltip::before { border: 5px solid silver; border-top-width: 0px; @@ -131,6 +134,26 @@ footnote::after { width: 0; } +.formattedTextBox-summarizer { + opacity :0.5; + position: relative; + width:40px; + height:20px; +} +.formattedTextBox-summarizer::after{ + content: "←" ; +} + +.formattedTextBox-summarizer-collapsed { + opacity :0.5; + position: relative; + width:40px; + height:20px; +} +.formattedTextBox-summarizer-collapsed::after { + content: "..."; +} + ol { counter-reset: deci1 0;} .decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 } .decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 } diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 9d83fbd04..8a623e648 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -13,9 +13,9 @@ import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc"; import { Copy, Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { RichTextField } from "../../../new_fields/RichTextField"; -import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types"; +import { BoolCast, Cast, NumCast, StrCast, DateCast, PromiseValue } from "../../../new_fields/Types"; import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { Utils, numberRange } from '../../../Utils'; +import { Utils, numberRange, timenow } from '../../../Utils'; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; @@ -40,6 +40,8 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { RichTextUtils } from '../../../new_fields/RichTextUtils'; import * as _ from "lodash"; import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment'; +import { inputRules } from 'prosemirror-inputrules'; +import { select } from 'async'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -77,6 +79,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private _editorView: Opt<EditorView>; private _applyingChange: boolean = false; private _linkClicked = ""; + private _nodeClicked: any; private _undoTyping?: UndoManager.Batch; private _reactionDisposer: Opt<IReactionDisposer>; private _searchReactionDisposer?: Lambda; @@ -118,6 +121,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @undoBatch public setFontColor(color: string) { + this._editorView!.state.storedMarks; if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false; if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) { this.props.Document.color = color; @@ -143,37 +147,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } - paste = (e: ClipboardEvent) => { - if (e.clipboardData && this._editorView) { - // let pdfPasteText = `${Utils.GenerateDeterministicGuid("pdf paste")}`; - // for (let i = 0; i < e.clipboardData.items.length; i++) { - // let item = e.clipboardData.items.item(i); - // console.log(item) - // if (item.type === "text/plain") { - // console.log("plain") - // item.getAsString((text) => { - // let pdfPasteIndex = text.indexOf(pdfPasteText); - // if (pdfPasteIndex > -1) { - // let insertText = text.substr(0, pdfPasteIndex); - // const tx = this._editorView!.state.tr.insertText(insertText); - // // tx.setSelection(new Selection(tx.)) - // const state = this._editorView!.state; - // this._editorView!.dispatch(tx); - // if (FormattedTextBox._toolTipTextMenu) { - // // this._toolTipTextMenu.makeLinkWithState(state) - // } - // e.stopPropagation(); - // e.preventDefault(); - // } - // }); - // } - // } - } - } - // this should be internal to prosemirror, but is needed // here to make sure that footnote view nodes in the overlay editor // get removed when they're not selected. + syncNodeSelection(view: any, sel: any) { if (sel instanceof NodeSelection) { var desc = view.docView.descAt(sel.from); @@ -195,6 +172,34 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe dispatchTransaction = (tx: Transaction) => { if (this._editorView) { + let metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata); + if (metadata) { + let range = tx.selection.$from.blockRange(tx.selection.$to); + let text = range ? tx.doc.textBetween(range.start, range.end) : ""; + let textEndSelection = tx.selection.to; + for (; textEndSelection < range!.end && text[textEndSelection - range!.start] !== " "; textEndSelection++) { } + text = text.substr(0, textEndSelection - range!.start); + text = text.split(" ")[text.split(" ").length - 1]; + let split = text.split("::"); + if (split.length > 1 && split[1]) { + let key = split[0]; + let value = split[split.length - 1]; + + let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); + DocServer.GetRefField(value).then(doc => { + DocServer.GetRefField(id).then(linkDoc => { + this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value); + if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; } + else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id); + }); + }); + const link = this._editorView!.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value }); + const mval = this._editorView!.state.schema.marks.metadataVal.create(); + let offset = (tx.selection.to === range!.end - 1 ? -1 : 0); + tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval); + this.dataDoc[key] = value; + } + } const state = this._editorView.state.apply(tx); this._editorView.updateState(state); this.syncNodeSelection(this._editorView, this._editorView.state.selection); // bcz: ugh -- shouldn't be needed but without this the overlay view's footnote popup doesn't get deselected @@ -207,12 +212,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now()))); this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON())); this._applyingChange = false; - let title = StrCast(this.dataDoc.title); - if (title && title.startsWith("-") && this._editorView && !this.Document.customTitle) { - let str = this._editorView.state.doc.textContent; - let titlestr = str.substr(0, Math.min(40, str.length)); - this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : ""); - } + this.updateTitle(); + this.tryUpdateHeight(); + } + } + + updateTitle = () => { + if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) { + let str = this._editorView.state.doc.textContent; + let titlestr = str.substr(0, Math.min(40, str.length)); + this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : ""); } } @@ -255,7 +264,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe // } } } - + setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => { + let view = this._editorView!; + let mid = view.state.doc.resolve(Math.round((start + end) / 2)); + let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened }); + view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid))); + } protected createDropTarget = (ele: HTMLDivElement) => { this._proseRef = ele; this.dropDisposer && this.dropDisposer(); @@ -267,10 +281,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe drop = async (e: Event, de: DragManager.DropEvent) => { // We're dealing with a link to a document if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) { + let target = de.data.embeddableSourceDoc; // We're dealing with an internal document drop let url = de.data.urlField.url.href; let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image; - this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url }))); + let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y }); + this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: target[Id] }))); + DocUtils.MakeLink(this.dataDoc, target, undefined, "ImgRef:" + target.title, undefined, undefined, target[Id]); e.stopPropagation(); } else if (de.data instanceof DragManager.DocumentDragData) { const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0]; @@ -335,14 +352,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create())); } + _keymap: any = undefined; @computed get config() { + this._keymap = buildKeymap(schema); return { schema, - inpRules, //these currently don't do anything, but could eventually be helpful plugins: this.props.isOverlay ? [ + inputRules(inpRules), this.tooltipTextMenuPlugin(), history(), - keymap(buildKeymap(schema)), + keymap(this._keymap), keymap(baseKeymap), // this.tooltipLinkingMenuPlugin(), new Plugin({ @@ -353,20 +372,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe formattedTextBoxCommentPlugin ] : [ history(), - keymap(buildKeymap(schema)), + keymap(this._keymap), keymap(baseKeymap), ] }; } - @action - rebuildEditor() { - this.setupEditor(this.config, this.dataDoc, this.props.fieldKey); - } - componentDidMount() { - document.addEventListener("paste", this.paste); - if (!this.props.isOverlay) { this._proxyReactionDisposer = reaction(() => this.props.isSelected(), () => { @@ -583,7 +595,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (link) { cbe.clipboardData!.setData("dash/linkDoc", link[Id]); linkId = link[Id]; - let frag = addMarkToFrag(slice.content); + let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(doc.title))); slice = new Slice(frag, slice.openStart, slice.openEnd); var tr = view.state.tr.replaceSelection(slice); view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")); @@ -593,24 +605,20 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return true; - function addMarkToFrag(frag: Fragment) { + function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) { const nodes: Node[] = []; - frag.forEach(node => nodes.push(addLinkMark(node))); + frag.forEach(node => nodes.push(marker(node))); return Fragment.fromArray(nodes); } - function addLinkMark(node: Node) { + function addLinkMark(node: Node, title: string) { if (!node.isText) { - const content = addMarkToFrag(node.content); + const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title)); return node.copy(content); } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type.name === "link"); - const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight" }); - if (linkIndex !== -1) { - marks.splice(linkIndex, 1, link); - } else { - marks.push(link); - } + const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight", title: title, docref: true }); + marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link); return node.mark(marks); } } @@ -628,14 +636,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } if (this._proseRef) { + let self = this; this._editorView && this._editorView.destroy(); this._editorView = new EditorView(this._proseRef, { state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), dispatchTransaction: this.dispatchTransaction, nodeViews: { - image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }, + image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); }, star(node, view, getPos) { return new SummarizedView(node, view, getPos); }, - ordered_list(node, view, getPos) { return new OrderedListView(node, view, getPos); }, + ordered_list(node, view, getPos) { return new OrderedListView(); }, footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); } }, clipboardTextSerializer: this.clipboardTextSerializer, @@ -648,15 +657,47 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - if (this.props.Document[Id] === FormattedTextBox.SelectOnLoad) { + let selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad; + if (selectOnLoad) { FormattedTextBox.SelectOnLoad = ""; this.props.select(false); } else if (this.props.isOverlay) this._editorView!.focus(); - var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks()); - let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail })]; - this._editorView!.state.storedMarks = newMarks; - + // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. + this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })]; + let heading = this.props.Document.heading; + if (heading && selectOnLoad) { + PromiseValue(Cast(this.props.Document.ruleProvider, Doc)).then(ruleProvider => { + if (ruleProvider) { + let align = StrCast(ruleProvider["ruleAlign_" + heading]); + let font = StrCast(ruleProvider["ruleFont_" + heading]); + let size = NumCast(ruleProvider["ruleSize_" + heading]); + if (align) { + let tr = this._editorView!.state.tr; + tr = tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(2))). + replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: align }), true). + setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(0))); + this._editorView!.dispatch(tr); + } + let sm = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })]; + size && (sm = [...sm, schema.marks.pFontSize.create({ fontSize: size })]); + font && (sm = [...sm, this.getFont(font)]); + this._editorView!.dispatch(this._editorView!.state.tr.setStoredMarks(sm)); + } + }); + } + } + getFont(font: string) { + switch (font) { + case "Arial": return schema.marks.arial.create(); + case "Times New Roman": return schema.marks.timesNewRoman.create(); + case "Georgia": return schema.marks.georgia.create(); + case "Comic Sans MS": return schema.marks.comicSans.create(); + case "Tahoma": return schema.marks.tahoma.create(); + case "Impact": return schema.marks.impact.create(); + case "ACrimson Textrial": return schema.marks.crimson.create(); + } + return schema.marks.arial.create(); } componentWillUnmount() { @@ -667,19 +708,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._pullReactionDisposer && this._pullReactionDisposer(); this._heightReactionDisposer && this._heightReactionDisposer(); this._searchReactionDisposer && this._searchReactionDisposer(); - document.removeEventListener("paste", this.paste); this._editorView && this._editorView.destroy(); } + onPointerDown = (e: React.PointerEvent): void => { + let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); + pos && (this._nodeClicked = this._editorView!.state.doc.nodeAt(pos.pos)); if (this.props.onClick && e.button === 0) { e.preventDefault(); } if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { e.stopPropagation(); - if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) { - //this._toolTipTextMenu.tooltip.style.opacity = "0"; - } } let ctrlKey = e.ctrlKey; if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) { @@ -691,6 +731,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) { href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href; } + let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); + let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); + if (node) { + let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link); + href = link && link.attrs.href; + location = link && link.attrs.location; + } if (href) { if (href.indexOf(Utils.prepend("/doc/")) === 0) { this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; @@ -711,7 +758,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); } else if (jumpToDoc) { DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); - + } else { + DocumentManager.Instance.jumpToDocument(linkDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); } } }); @@ -740,13 +788,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => { - let view = this._editorView!; - let mid = view.state.doc.resolve(Math.round((start + end) / 2)); - let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened }); - view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid, mid))); - } - @action onFocused = (e: React.FocusEvent): void => { document.removeEventListener("keypress", this.recordKeyHandler); @@ -768,6 +809,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } onClick = (e: React.MouseEvent): void => { + // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. + if (this.props.isSelected() && e.nativeEvent.offsetX < 40) { + let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); + if (pos && pos.pos > 0) { + let node = this._editorView!.state.doc.nodeAt(pos.pos); + let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined; + if (node === this._nodeClicked && node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) { + this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility })); + } + } + } this._proseRef!.focus(); if (this._linkClicked) { this._linkClicked = ""; @@ -811,39 +863,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe SelectionManager.DeselectAll(); } e.stopPropagation(); - if (e.key === "Tab" || e.key === "Enter") { // bullets typically change "levels" when tab or enter is used. sometimes backspcae, so maybe that should be added. + if (e.key === "Tab" || e.key === "Enter") { e.preventDefault(); } - function timenow() { - var now = new Date(); - let ampm = 'am'; - let h = now.getHours(); - let m: any = now.getMinutes(); - let s: any = now.getSeconds(); - if (h >= 12) { - if (h > 12) h -= 12; - ampm = 'pm'; - } - - if (m < 10) m = '0' + m; - return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm; - } - var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks()); - let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })]; - this._editorView!.state.storedMarks = newMarks; + this._editorView!.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })); - // stop propagation doesn't seem to stop propagation of native keyboard events. - // so we set a flag on the native event that marks that the event's been handled. - (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; - if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) { - let str = this._editorView.state.doc.textContent; - let titlestr = str.substr(0, Math.min(40, str.length)); - this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : ""); - } if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); } - this.tryUpdateHeight(); } @action @@ -860,7 +887,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe render() { - let self = this; let style = this.props.isOverlay ? "scroll" : "hidden"; let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : ""; let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground || diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx index 255000936..2d2fff06d 100644 --- a/src/client/views/nodes/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -92,7 +92,7 @@ export class FormattedTextBoxComment { if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return; - let set = "none" + let set = "none"; if (state.selection.$from) { let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark); let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark); @@ -130,7 +130,7 @@ export class FormattedTextBoxComment { if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) { let docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0]; docTarget && DocServer.GetRefField(docTarget).then(linkDoc => - (linkDoc as Doc) && (FormattedTextBoxComment.tooltipText.textContent = "link :" + StrCast((linkDoc as Doc)!.title))); + (linkDoc as Doc) && (FormattedTextBoxComment.tooltipText.textContent = "link :" + StrCast((linkDoc as Doc).title))); } // These are in screen coordinates // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 5afd4d834..a27dbd83d 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -112,7 +112,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { <div className="keyValuePair-td-value-container"> <EditableView contents={contents} - height={36} + maxHeight={36} + height={"auto"} GetValue={() => { return Field.toKeyValueString(props.Document, props.fieldKey); }} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 18f82ff47..df35b603c 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -33,7 +33,9 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>; @computed get containingCollectionDocument() { return this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; } - @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } + @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + + @computed get fieldExtensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); } private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); @@ -48,7 +50,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen componentDidMount() { this.props.setPdfBox && this.props.setPdfBox(this); - const pdfUrl = Cast(this.props.Document.data, PdfField); + const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); if (pdfUrl instanceof PdfField) { Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf)); } @@ -78,7 +80,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen @action public GotoPage(p: number) { - if (p > 0 && p <= NumCast(this.props.Document.numPages)) { + if (p > 0 && p <= NumCast(this.dataDoc.numPages)) { this.props.Document.curPage = p; this.props.Document.panY = (p - 1) * NumCast(this.dataDoc.nativeHeight); } @@ -87,7 +89,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen @action public ForwardPage() { let cp = this.GetPage() + 1; - if (cp <= NumCast(this.props.Document.numPages)) { + if (cp <= NumCast(this.dataDoc.numPages)) { this.props.Document.curPage = cp; this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight); } @@ -185,11 +187,12 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen } } + render() { - const pdfUrl = Cast(this.props.Document.data, PdfField); + const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); return (!(pdfUrl instanceof PdfField) || !this._pdf ? - <div>{`pdf, ${this.props.Document.data}, not found`}</div> : + <div>{`pdf, ${this.dataDoc[this.props.fieldKey]}, not found`}</div> : <div className={classname} onScroll={this.onScroll} style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }} diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index 43220df71..fbe9bf063 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -30,6 +30,7 @@ width: 100%; height: 100%; position: absolute; + pointer-events: all; } .webBox-button { diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 6f77a0a5b..eeb2531a2 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -1,5 +1,5 @@ import React = require("react"); -import { action, IReactionDisposer, observable, reaction } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; @@ -13,10 +13,7 @@ import { PresBox } from "../nodes/PresBox"; interface IAnnotationProps { anno: Doc; - index: number; - ParentIndex: () => number; fieldExtensionDoc: Doc; - scrollTo?: (n: number) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; pinToPres: (document: Doc) => void; } @@ -33,10 +30,7 @@ interface IRegionAnnotationProps { y: number; width: number; height: number; - index: number; - ParentIndex: () => number; fieldExtensionDoc: Doc; - scrollTo?: (n: number) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; pinToPres: (document: Doc) => void; document: Doc; @@ -45,9 +39,11 @@ interface IRegionAnnotationProps { @observer class RegionAnnotation extends React.Component<IRegionAnnotationProps> { private _reactionDisposer?: IReactionDisposer; - private _scrollDisposer?: IReactionDisposer; + private _brushDisposer?: IReactionDisposer; private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + @observable private _brushed: boolean = false; + componentDidMount() { this._reactionDisposer = reaction( () => this.props.document.delete, @@ -55,15 +51,18 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { { fireImmediately: true } ); - this._scrollDisposer = reaction( - () => this.props.ParentIndex(), - (ind) => ind === this.props.index && this.props.scrollTo && this.props.scrollTo(this.props.y * scale) - ); + this._brushDisposer = reaction( + () => FieldValue(Cast(this.props.document.group, Doc)) && Doc.IsBrushed(FieldValue(Cast(this.props.document.group, Doc))!), + (brushed) => { + if (brushed !== undefined) { + runInAction(() => this._brushed = brushed); + } + } + ) } componentWillUnmount() { this._reactionDisposer && this._reactionDisposer(); - this._scrollDisposer && this._scrollDisposer(); } deleteAnnotation = () => { @@ -126,7 +125,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { left: this.props.x, width: this.props.width, height: this.props.height, - backgroundColor: this.props.ParentIndex() === this.props.index ? "green" : StrCast(this.props.document.color) + backgroundColor: this._brushed ? "green" : StrCast(this.props.document.color) }} />); } }
\ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index e5917fefc..7bc1d3507 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -76,7 +76,15 @@ export class PDFViewer extends React.Component<IViewerProps> { return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.panY + (this._pageSizes[0] ? this._pageSizes[0].height : 0)) + this._pageBuffer); } - @computed get filteredAnnotations() { + @computed get allAnnotations() { + let annotations = DocListCast(this.props.fieldExtensionDoc.annotations); + return annotations.filter(anno => { + let run = this._script.run({ this: anno }); + return run.success ? run.result : true; + }) + } + + @computed get nonDocAnnotations() { return this._annotations.filter(anno => { let run = this._script.run({ this: anno }); return run.success ? run.result : true; @@ -101,12 +109,15 @@ export class PDFViewer extends React.Component<IViewerProps> { this._filterReactionDisposer = reaction( () => ({ scriptField: Cast(this.props.Document.filterScript, ScriptField), annos: this._annotations.slice() }), action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => { + let oldScript = this._script.originalScript; this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript; + if (this._script.originalScript !== oldScript) { + this.Index = -1; + } annos.forEach(d => { let run = this._script.run(d); d.opacity = !run.success || run.result ? 1 : 0; }); - this.Index = -1; }), { fireImmediately: true } ); @@ -170,6 +181,7 @@ export class PDFViewer extends React.Component<IViewerProps> { let startY = NumCast(this.props.Document.startY, NumCast(this.props.Document.panY)); this.props.setPanY && this.props.setPanY(startY); + this.props.scrollTo(startY); } } @@ -295,12 +307,20 @@ export class PDFViewer extends React.Component<IViewerProps> { prevAnnotation = (e: React.MouseEvent) => { e.stopPropagation(); this.Index = Math.max(this.Index - 1, 0); + let scrollToAnnotation = this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index]; + this.allAnnotations.forEach(d => Doc.UnBrushDoc(d)); + Doc.BrushDoc(scrollToAnnotation); + this.props.scrollTo(NumCast(scrollToAnnotation.y)); } @action nextAnnotation = (e: React.MouseEvent) => { e.stopPropagation(); - this.Index = Math.min(this.Index + 1, this.filteredAnnotations.length - 1); + this.Index = Math.min(this.Index + 1, this.allAnnotations.length - 1); + let scrollToAnnotation = this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index]; + this.allAnnotations.forEach(d => Doc.UnBrushDoc(d)); + Doc.BrushDoc(scrollToAnnotation); + this.props.scrollTo(NumCast(scrollToAnnotation.y)); } sendAnnotations = (page: number) => { @@ -413,8 +433,8 @@ export class PDFViewer extends React.Component<IViewerProps> { </div> <div className="pdfViewer-text" ref={this._viewer} /> <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}> - {this.filteredAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) => - <Annotation {...this.props} ParentIndex={this.getIndex} anno={anno} index={index} key={`${anno[Id]}-annotation`} />)} + {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) => + <Annotation {...this.props} anno={anno} key={`${anno[Id]}-annotation`} />)} </div> <div className="pdfViewer-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ bottom: -this.props.panY, left: `${this._searching ? 0 : 100}%` }}> diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx index 8df2dce29..856e883e7 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -64,7 +64,7 @@ export default class Page extends React.Component<IPageProps> { // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes if (this._state !== "rendering" && !this._page && this._canvas.current && this._textLayer.current) { this._state = "rendering"; - let viewport = page.getViewport({ scale: scale }); + let viewport = page.getViewport(scale); this._canvas.current.width = this._width = viewport.width; this._canvas.current.height = this._height = viewport.height; this.props.pageLoaded(viewport); diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 2ad69daca..0d50124dd 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,25 +1,23 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { observable, action, runInAction, flow, computed } from 'mobx'; -import "./SearchBox.scss"; -import "./FilterBox.scss"; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { library } from '@fortawesome/fontawesome-svg-core'; -import { SetupDrag } from '../../util/DragManager'; -import { Docs } from '../../documents/Documents'; -import { NumCast, Cast } from '../../../new_fields/Types'; -import { Doc } from '../../../new_fields/Doc'; -import { SearchItem } from './SearchItem'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; import * as rp from 'request-promise'; +import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; -import { SearchUtil } from '../../util/SearchUtil'; +import { Cast, NumCast } from '../../../new_fields/Types'; import { RouteStore } from '../../../server/RouteStore'; -import { FilterBox } from './FilterBox'; -import { ReadStream } from 'fs'; -import * as $ from 'jquery'; -import { MainView } from '../MainView'; import { Utils } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; +import { SetupDrag } from '../../util/DragManager'; +import { SearchUtil } from '../../util/SearchUtil'; +import { MainView } from '../MainView'; +import { FilterBox } from './FilterBox'; +import "./FilterBox.scss"; +import "./SearchBox.scss"; +import { SearchItem } from './SearchItem'; library.add(faTimes); @@ -141,7 +139,7 @@ export class SearchBox extends React.Component { private get filterQuery() { const types = FilterBox.Instance.filterTypes; const includeDeleted = FilterBox.Instance.getDataStatus(); - return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : ""); + return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted_b:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : ""); } @@ -222,7 +220,6 @@ export class SearchBox extends React.Component { doc.width = size; doc.height = size; } - doc.zoomBasis = 1; x += 250; if (x > 1000) { x = 0; @@ -304,14 +301,16 @@ export class SearchBox extends React.Component { this.getResults(this._searchString); if (i < this._results.length) result = this._results[i]; if (result) { - this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />; + let highlights = Array.from([...Array.from(new Set(result[1]).values())]).filter(v => v !== "search_string"); + this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} highlighting={highlights} />; this._isSearch[i] = "search"; } } else { result = this._results[i]; if (result) { - this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />; + let highlights = Array.from([...Array.from(new Set(result[1]).values())]).filter(v => v !== "search_string"); + this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} highlighting={highlights} />; this._isSearch[i] = "search"; } } diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 386b5fe74..c56d093fa 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -28,7 +28,7 @@ import "./SelectorContextMenu.scss"; export interface SearchItemProps { doc: Doc; - query?: string; + query: string; highlighting: string[]; } @@ -71,8 +71,8 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> { return () => { col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { - const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2; - const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2; + const newPanX = NumCast(target.x) + NumCast(target.width) / 2; + const newPanY = NumCast(target.y) + NumCast(target.height) / 2; col.panX = newPanX; col.panY = newPanY; } @@ -128,68 +128,31 @@ export class LinkContextMenu extends React.Component<LinkMenuProps> { export class SearchItem extends React.Component<SearchItemProps> { @observable _selected: boolean = false; - private _previewDoc?: Doc; onClick = () => { // I dont think this is the best functionality because clicking the name of the collection does that. Change it back if you'd like DocumentManager.Instance.jumpToDocument(this.props.doc, false); - if (this.props.doc.data instanceof RichTextField) { - this.highlightTextBox(this.props.doc); - } - // CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined); } @observable _useIcons = true; @observable _displayDim = 50; - highlightTextBox = (doc: Doc) => { - if (this.props.query) { - const fieldkey = 'search_string'; - if (Object.keys(doc).indexOf(fieldkey) === -1) { - doc.search_string = this.props.query; - } - else { - doc.search_string = undefined; - } - - } - } - - fitToBox = () => { - let bounds = Doc.ComputeContentBounds([this.props.doc]); - return [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / Math.max((bounds.b - bounds.y), (bounds.r - bounds.x)), this._displayDim]; + componentDidMount() { + this.props.doc.search_string = this.props.query; + this.props.doc.search_fields = this.props.highlighting.join(", "); } - componentWillUnmount() { - if (this._previewDoc) { - DocServer.DeleteDocument(this._previewDoc[Id]); - } + this.props.doc.search_string = undefined; + this.props.doc.search_fields = undefined; } - //@computed @action public DocumentIcon() { let layoutresult = StrCast(this.props.doc.type); if (!this._useIcons) { - let renderDoc = this.props.doc; - //let box: number[] = []; - if (layoutresult.indexOf(DocumentType.COL) !== -1) { - renderDoc = Doc.MakeDelegate(renderDoc); - let bounds = DocListCast(renderDoc.data).reduce((bounds, doc) => { - var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; - let [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()]; - return { - x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), - r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) - }; - }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); - let box = () => [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / (bounds.r - bounds.x), this._displayDim]; - } let returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE); let returnYDimension = () => this._displayDim; - let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension()); - let newRenderDoc = Doc.MakeDelegate(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt - this._previewDoc = newRenderDoc; + let scale = () => returnXDimension() / NumCast(this.props.doc.nativeWidth, returnXDimension()); const docview = <div onPointerDown={action(() => { this._useIcons = !this._useIcons; @@ -219,15 +182,8 @@ export class SearchItem extends React.Component<SearchItemProps> { ContentScaling={scale} /> </div>; - const data = renderDoc.data; - if (data instanceof ObjectField) newRenderDoc.data = ObjectField.MakeCopy(data); - newRenderDoc.preview = true; - newRenderDoc.search_string = this.props.query; return docview; } - if (this._previewDoc) { - DocServer.DeleteDocument(this._previewDoc[Id]); - } let button = layoutresult.indexOf(DocumentType.PDF) !== -1 ? faFilePdf : layoutresult.indexOf(DocumentType.IMG) !== -1 ? faImage : layoutresult.indexOf(DocumentType.TEXT) !== -1 ? faStickyNote : @@ -279,8 +235,7 @@ export class SearchItem extends React.Component<SearchItemProps> { Doc.BrushDoc(doc2); } } else { - DocumentManager.Instance.getAllDocumentViews(this.props.doc).forEach(element => - Doc.BrushDoc(element.props.Document)); + Doc.BrushDoc(this.props.doc); } } @@ -294,8 +249,7 @@ export class SearchItem extends React.Component<SearchItemProps> { Doc.UnBrushDoc(doc2); } } else { - DocumentManager.Instance.getAllDocumentViews(this.props.doc). - forEach(element => Doc.UnBrushDoc(element.props.Document)); + Doc.UnBrushDoc(this.props.doc); } } diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index a703f1cef..e94b9f1eb 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -300,15 +300,15 @@ export namespace Doc { export function AreProtosEqual(doc?: Doc, other?: Doc) { if (!doc || !other) return false; let r = (doc === other); - let r2 = (doc.proto === other); - let r3 = (other.proto === doc); - let r4 = (doc.proto === other.proto && other.proto !== undefined); + let r2 = (Doc.GetProto(doc) === other); + let r3 = (Doc.GetProto(other) === doc); + let r4 = (Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined); return r || r2 || r3 || r4; } // gets the document's prototype or returns the document if it is a prototype export function GetProto(doc: Doc) { - return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc); + return doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc)); } export function GetDataDoc(doc: Doc): Doc { let proto = Doc.GetProto(doc); @@ -327,6 +327,9 @@ export namespace Doc { return Array.from(results); } + export function IndexOf(toFind: Doc, list: Doc[]) { + return list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind)) + } export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) { if (target[key] === undefined) { Doc.GetProto(target)[key] = new List<Doc>(); @@ -522,9 +525,10 @@ export namespace Doc { otherdoc.type = DocumentType.TEMPLATE; !templateDoc.nativeWidth && (otherdoc.nativeWidth = 0); !templateDoc.nativeHeight && (otherdoc.nativeHeight = 0); + !templateDoc.nativeWidth && (otherdoc.ignoreAspect = true); return otherdoc; } - export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetData?: Doc) { + export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetData?: Doc, useTemplateDoc?: boolean) { if (!templateDoc) { target.layout = undefined; target.nativeWidth = undefined; @@ -533,11 +537,12 @@ export namespace Doc { target.type = undefined; return; } - let temp = Doc.MakeDelegate(templateDoc); + let temp = useTemplateDoc ? templateDoc : Doc.MakeDelegate(templateDoc); target.nativeWidth = Doc.GetProto(target).nativeWidth = undefined; target.nativeHeight = Doc.GetProto(target).nativeHeight = undefined; !templateDoc.nativeWidth && (target.nativeWidth = 0); !templateDoc.nativeHeight && (target.nativeHeight = 0); + !templateDoc.nativeHeight && (target.ignoreAspect = true); target.width = templateDoc.width; target.height = templateDoc.height; target.onClick = templateDoc.onClick instanceof ObjectField && templateDoc.onClick[Copy](); @@ -576,10 +581,13 @@ export namespace Doc { /* move certain layout properties from the original data doc to the template layout to avoid inheriting them from the template's data doc which may also define these fields for its own use. */ - fieldTemplate.ignoreAspect = BoolCast(fieldTemplate.ignoreAspect); + fieldTemplate.ignoreAspect = fieldTemplate.ignoreAspect === undefined ? undefined : BoolCast(fieldTemplate.ignoreAspect); fieldTemplate.singleColumn = BoolCast(fieldTemplate.singleColumn); fieldTemplate.nativeWidth = Cast(fieldTemplate.nativeWidth, "number"); fieldTemplate.nativeHeight = Cast(fieldTemplate.nativeHeight, "number"); + fieldTemplate.panX = 0; + fieldTemplate.panY = 0; + fieldTemplate.scale = 1; fieldTemplate.showTitle = "title"; setTimeout(() => fieldTemplate.proto = templateDataDoc); } @@ -680,4 +688,5 @@ export namespace Doc { Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(doc.title).replace(/\([0-9]*\)/, "") + `(${n})`; }); Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); }); Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); }); -Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); });
\ No newline at end of file +Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); }); +Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
\ No newline at end of file diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py index 807216ef1..a9256073b 100644 --- a/src/scraping/buxton/scraper.py +++ b/src/scraping/buxton/scraper.py @@ -88,7 +88,6 @@ def write_collection(parse_results, display_fields, storage_key, viewType=2): "height": 600, "panX": 0, "panY": 0, - "zoomBasis": 1, "zIndex": 2, "libraryBrush": False, "viewType": viewType diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 51642e345..c2656cc1c 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -6,7 +6,6 @@ import * as path from 'path'; import { Opt } from '../../../new_fields/Doc'; import * as sharp from 'sharp'; import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes'; -import { reject } from 'bluebird'; const uploadDirectory = path.join(__dirname, "../../public/files/"); @@ -51,7 +50,7 @@ export namespace GooglePhotosUploadUtils { uri: prepend('uploads'), body }; - return new Promise<any>(resolve => request(parameters, (error, _response, body) => { + return new Promise<any>((resolve, reject) => request(parameters, (error, _response, body) => { if (error) { return reject(error); } @@ -106,11 +105,12 @@ export namespace DownloadUtils { LARGE: { width: 900, suffix: "_l" }, }; - const png = ".png"; - const pngs = [".png", ".PNG"]; - const jpgs = [".jpg", ".JPG", ".jpeg", ".JPEG"]; - const imageFormats = [".jpg", ".png", ".gif"]; + const gifs = [".gif"]; + const pngs = [".png"]; + const jpgs = [".jpg", ".jpeg"]; + const imageFormats = [...pngs, ...jpgs, ...gifs]; const videoFormats = [".mov", ".mp4"]; + const size = "content-length"; const type = "content-type"; @@ -126,7 +126,7 @@ export namespace DownloadUtils { export const UploadImage = async (url: string, filename?: string, prefix = ""): Promise<Opt<UploadInformation>> => { const resolved = filename ? sanitize(filename) : generate(prefix, url); - const extension = path.extname(url) || path.extname(resolved) || png; + const extension = (path.extname(url) || path.extname(resolved)).toLowerCase() || ".png"; let information: UploadInformation = { mediaPaths: [], fileNames: { clean: resolved } diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 9866e22eb..af5774ebe 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -7,7 +7,7 @@ import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/nort import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil"; import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView"; import { CollectionView } from "../../../client/views/collections/CollectionView"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types"; @@ -50,6 +50,16 @@ export class CurrentUserUtils { doc.workspaces = workspaces; } PromiseValue(Cast(doc.workspaces, Doc)).then(workspaces => workspaces && (workspaces.preventTreeViewOpen = true)); + if (doc.noteTypes === undefined) { + let notes = [Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplate: true }), + Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplate: true }), + Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplate: true }), + Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplate: true })]; + const noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 }); + noteTypes.excludeFromLibrary = true; + doc.noteTypes = noteTypes; + } + PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(vals => DocListCast(vals))); if (doc.recentlyClosed === undefined) { const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 }); recentlyClosed.excludeFromLibrary = true; @@ -115,17 +125,17 @@ export class CurrentUserUtils { throw new Error("There should be a user id! Why does Dash think there isn't one?"); } }); - try { - const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" }); - NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json()); - await Gateway.Instance.ClearCatalog(); - const extraSchemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []); - let extras = await Promise.all(extraSchemas.map(sc => Gateway.Instance.GetSchema("", sc))); - let catprom = CurrentUserUtils.SetNorthstarCatalog(await Gateway.Instance.GetCatalog(), extras); - // if (catprom) await Promise.all(catprom); - } catch (e) { + // try { + // const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" }); + // NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json()); + // await Gateway.Instance.ClearCatalog(); + // const extraSchemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []); + // let extras = await Promise.all(extraSchemas.map(sc => Gateway.Instance.GetSchema("", sc))); + // let catprom = CurrentUserUtils.SetNorthstarCatalog(await Gateway.Instance.GetCatalog(), extras); + // // if (catprom) await Promise.all(catprom); + // } catch (e) { - } + // } } /* Northstar catalog ... really just for testing so this should eventually go away */ diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 7442c643a..fec1625f5 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.GlyBB9xlRimL3pw4tgNg7g7wcr73JWyQd4-XZbgOvngFM_sYUgsWP0YV7XCez5u6nytEfrOm228Sadj52wluJ46cJGhj2IwtSbW9GYzHHiiD-ts0i1phIV3n28wo5A","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568254634977}
\ No newline at end of file +{"access_token":"ya29.GlyBB1MlCG7GL2pYFleLp9uUJoN6s0_PFBDLUIhyrKAY4kkVo7vbuaW_zmkJs1Fym0f7NVpaYvFsBK2dbN6Qn5P8bWNW2NsHNNGcwbyGIS8H52GUlyCsawNt6PTnOw","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568274162450}
\ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 9a2bd9a3a..101a4f63f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -451,7 +451,7 @@ function LoadPage(file: string, pageNumber: number, res: Response) { console.log(pageNumber); pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => { console.log("reading " + page); - let viewport = page.getViewport(1); + let viewport = page.getViewport(1 as any); let canvasAndContext = factory.create(viewport.width, viewport.height); let renderContext = { canvasContext: canvasAndContext.context, @@ -796,9 +796,9 @@ const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerU ]); app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { - let sector: GoogleApiServerUtils.Service = req.params.sector; - let action: GoogleApiServerUtils.Action = req.params.action; - GoogleApiServerUtils.GetEndpoint(sector, { credentialsPath, tokenPath }).then(endpoint => { + let sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service; + let action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action; + GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentialsPath, tokenPath }).then(endpoint => { let handler = EndpointHandlerMap.get(action); if (endpoint && handler) { let execute = handler(endpoint, req.body).then( |