aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts17
-rw-r--r--src/client/util/ParagraphNodeSpec.ts133
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts133
-rw-r--r--src/client/util/RichTextRules.ts115
-rw-r--r--src/client/util/RichTextSchema.tsx443
-rw-r--r--src/client/util/TooltipLinkingMenu.tsx20
-rw-r--r--src/client/util/TooltipTextMenu.tsx74
-rw-r--r--src/client/util/clamp.js15
-rw-r--r--src/client/util/convertToCSSPTValue.js43
-rw-r--r--src/client/util/prosemirrorPatches.js139
-rw-r--r--src/client/util/toCSSLineSpacing.js64
-rw-r--r--src/client/views/ContextMenu.tsx5
-rw-r--r--src/client/views/ContextMenuItem.tsx2
-rw-r--r--src/client/views/DocumentDecorations.tsx14
-rw-r--r--src/client/views/InkingControl.tsx37
-rw-r--r--src/client/views/MainOverlayTextBox.tsx6
-rw-r--r--src/client/views/MainView.tsx17
-rw-r--r--src/client/views/OverlayView.scss5
-rw-r--r--src/client/views/OverlayView.tsx87
-rw-r--r--src/client/views/PreviewCursor.tsx6
-rw-r--r--src/client/views/ScriptBox.tsx15
-rw-r--r--src/client/views/SearchItem.tsx67
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx4
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx2
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx4
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx4
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx97
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx32
-rw-r--r--src/client/views/linking/LinkEditor.scss (renamed from src/client/views/nodes/LinkEditor.scss)0
-rw-r--r--src/client/views/linking/LinkEditor.tsx (renamed from src/client/views/nodes/LinkEditor.tsx)0
-rw-r--r--src/client/views/linking/LinkFollowBox.scss93
-rw-r--r--src/client/views/linking/LinkFollowBox.tsx611
-rw-r--r--src/client/views/linking/LinkMenu.scss (renamed from src/client/views/nodes/LinkMenu.scss)0
-rw-r--r--src/client/views/linking/LinkMenu.tsx (renamed from src/client/views/nodes/LinkMenu.tsx)3
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx (renamed from src/client/views/nodes/LinkMenuGroup.tsx)12
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx (renamed from src/client/views/nodes/LinkMenuItem.tsx)53
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx39
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx4
-rw-r--r--src/client/views/nodes/DocumentView.tsx35
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss94
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx292
-rw-r--r--src/client/views/nodes/FormattedTextBoxComment.tsx104
-rw-r--r--src/client/views/nodes/PDFBox.tsx15
-rw-r--r--src/client/views/nodes/WebBox.scss1
-rw-r--r--src/client/views/nodes/WebBox.tsx23
-rw-r--r--src/client/views/pdf/Annotation.tsx27
-rw-r--r--src/client/views/pdf/PDFViewer.tsx30
-rw-r--r--src/client/views/pdf/Page.tsx2
-rw-r--r--src/client/views/search/SearchItem.tsx1
50 files changed, 2424 insertions, 616 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 1578e49fe..381981e1b 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -19,4 +19,5 @@ export enum DocumentType {
YOUTUBE = "youtube",
DRAGBOX = "dragbox",
PRES = "presentation",
+ LINKFOLLOW = "linkfollow",
} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 0d0c162a1..e53c72c1c 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -45,6 +45,7 @@ import { PresBox } from "../views/nodes/PresBox";
import { ComputedField } from "../../new_fields/ScriptField";
import { ProxyField } from "../../new_fields/Proxy";
import { DocumentType } from "./DocumentTypes";
+import { LinkFollowBox } from "../views/linking/LinkFollowBox";
//import { PresBox } from "../views/nodes/PresBox";
//import { PresField } from "../../new_fields/PresField";
var requestImageSize = require('../util/request-image-size');
@@ -53,6 +54,7 @@ var path = require('path');
export interface DocumentOptions {
x?: number;
y?: number;
+ z?: number;
type?: string;
width?: number;
height?: number;
@@ -170,6 +172,9 @@ export namespace Docs {
[DocumentType.DRAGBOX, {
layout: { view: DragBox },
options: { width: 40, height: 40 },
+ }],
+ [DocumentType.LINKFOLLOW, {
+ layout: { view: LinkFollowBox }
}]
]);
@@ -414,8 +419,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) {
@@ -443,6 +448,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.DRAGBOX), undefined, { ...(options || {}) });
}
+ export function LinkFollowBoxDocument(options?: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.LINKFOLLOW), undefined, { ...(options || {}) });
+ }
+
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
}
@@ -597,13 +606,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/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 3979d8a49..1d2d33800 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -2,7 +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, 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";
@@ -50,6 +51,19 @@ 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;
+ // });
+
let cmd = chainCommands(exitCode, (state, dispatch) => {
if (dispatch) {
@@ -79,89 +93,65 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
bind("Mod-s", TooltipTextMenu.insertStar);
- let nodeTypeMark = (depth: number) => depth === 2 ? "indent2" : depth === 4 ? "indent3" : depth === 6 ? "indent4" : "indent1";
+ 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);
+ }
+ });
+ };
+
- let bulletFunc = (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ 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());
- let depth = range && range.depth ? range.depth : 0;
if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
- const resolvedPos = tx2.doc.resolve(range!.start);
-
- let path = (resolvedPos as any).path;
- for (let i = path.length - 1; i > 0; i--) {
- if (path[i].type === schema.nodes.ordered_list) {
- path[i].attrs.bulletStyle = nodeTypeMark(depth);
- break;
- }
- }
+ updateBullets(tx2);
marks && tx2.ensureMarks([...marks]);
marks && tx2.setStoredMarks([...marks]);
dispatch(tx2);
- })) {
- let sxf = state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end));
- let newstate = state.applyTransaction(sxf);
+ })) { // couldn't sink into an existing list, so wrap in a new one
+ 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) => {
- const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2));
- let path = (resolvedPos as any).path;
- for (let i = path.length - 1; i > 0; i--) {
- if (path[i].type === schema.nodes.ordered_list) {
- path[i].attrs.bulletStyle = nodeTypeMark(depth);
- 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]);
-
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
dispatch(tx2);
})) {
- console.log("bullet fail");
+ console.log("bullet promote fail");
}
}
- };
-
- bind("Tab", bulletFunc);
+ });
bind("Shift-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());
- let depth = range && range.depth > 3 ? range.depth - 4 : 0;
- liftListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
- try {
- const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2));
-
- let path = (resolvedPos as any).path;
- for (let i = path.length - 1; i > 0; i--) {
- if (path[i].type === schema.nodes.ordered_list) {
- path[i].attrs.bulletStyle = nodeTypeMark(depth);
- break;
- }
- }
- marks && tx2.ensureMarks([...marks]);
- marks && tx2.setStoredMarks([...marks]);
- dispatch(tx2);
- } catch (e) {
- dispatch(tx2);
- }
- });
+ if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
+ updateBullets(tx2);
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
+ dispatch(tx2);
+ })) {
+ console.log("bullet demote fail");
+ }
});
+ 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);
- if (!liftListItem(schema.nodes.list_item)(state, dispatch as ((tx: Transaction<Schema<any, any>>) => void))
- ) {
+ splitMetadata(marks, tx3);
+ if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
dispatch(tx3);
}
})) {
@@ -170,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 df33589bf..379da112f 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,7 +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 } from "prosemirror-state";
+import { EditorState, TextSelection } from "prosemirror-state";
+import { StepMap } from "prosemirror-transform";
+import { EditorView } from "prosemirror-view";
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];
@@ -14,23 +25,32 @@ export const nodes: { [index: string]: NodeSpec } = {
content: "block+"
},
- // :: 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" }]
+ footnote: {
+ group: "inline",
+ content: "inline*",
+ inline: true,
+ attrs: {
+ visibility: { default: false }
+ },
+ // This makes the view treat the node as a leaf, even though it
+ // technically has content
+ atom: true,
+ toDOM: () => ["footnote", 0],
+ 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; }
// },
+ paragraph: ParagraphNodeSpec,
+
// :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
blockquote: {
content: "block+",
@@ -88,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) {
@@ -113,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,
@@ -129,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 }];
@@ -176,47 +196,45 @@ export const nodes: { [index: string]: NodeSpec } = {
content: 'list_item+',
group: 'block',
attrs: {
- bulletStyle: { default: "" },
- mapStyle: { default: "decimal" }
+ bulletStyle: { default: 0 },
+ mapStyle: { default: "decimal" },
+ visibility: { default: true }
},
toDOM(node: Node<any>) {
const bs = node.attrs.bulletStyle;
- const decMap = bs === "indent1" ? "decimal" : bs === "indent2" ? "decimal2" : bs === "indent3" ? "decimal3" : bs === "indent4" ? "decimal4" : "";
- const multiMap = bs === "indent1" ? "decimal" : bs === "indent2" ? "upper-alpha" : bs === "indent3" ? "lower-roman" : bs === "indent4" ? "lower-alpha" : "";
+ 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 ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle};`, 0]
+ 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];
}
},
};
@@ -224,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 } = {
@@ -236,7 +253,8 @@ export const marks: { [index: string]: MarkSpec } = {
href: {},
location: { default: null },
title: { default: null },
- guid: { default: null }
+ guid: { default: null },
+ docref: { default: false }
},
inclusive: false,
parseDOM: [{
@@ -244,7 +262,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.
@@ -263,16 +285,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' },
@@ -313,15 +325,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: 'color: blue' }],
+ 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: 'color: blue'
+ 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() {
@@ -340,14 +405,13 @@ 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);
return hidden ?
(node.attrs.opened ?
['span', { class: "userMarkOpen" }, 0] :
- ['span', { class: "userMark" }, ['span', { style: "font-size:2" }, 0]]
+ ['span', { class: "userMark" }, ['span', 0]]
) :
['span', 0];
}
@@ -360,6 +424,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: {
@@ -504,15 +586,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");
@@ -520,6 +599,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%";
@@ -532,6 +612,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();
@@ -541,15 +648,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);
@@ -574,74 +684,186 @@ export class ImageResizeView {
}
}
+export class OrderedListView {
+ 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
+ }
+}
+
+export class FootnoteView {
+ innerView: any;
+ outerView: any;
+ node: any;
+ dom: any;
+ getPos: any;
+
+ constructor(node: any, view: any, getPos: any) {
+ // We'll need these later
+ 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;
+ }
+ selectNode() {
+ const attrs = { ...this.node.attrs };
+ attrs.visibility = true;
+ 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();
+ }
+ open() {
+ if (!this.outerView.isOverlay) return;
+ // Append a tooltip to the outer node
+ let tooltip = this.dom.appendChild(document.createElement("div"));
+ tooltip.className = "footnote-tooltip";
+ // And put a sub-ProseMirror into that
+ this.innerView = new EditorView(tooltip, {
+ // You can use any node as an editor document
+ state: EditorState.create({
+ doc: this.node,
+ plugins: [keymap(baseKeymap),
+ keymap({
+ "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
+ "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
+ "Mod-b": toggleMark(schema.marks.strong)
+ })]
+ }),
+ // This is the magic part
+ dispatchTransaction: this.dispatchInner.bind(this),
+ handleDOMEvents: {
+ pointerdown: ((view: any, e: PointerEvent) => {
+ // Kludge to prevent issues due to the fact that the whole
+ // footnote is node-selected (and thus DOM-selected) when
+ // the parent editor is focused.
+ e.stopPropagation();
+ document.addEventListener("pointerup", this.ignore, true);
+ if (this.outerView.hasFocus()) this.innerView.focus();
+ }) as any
+ }
+
+ });
+ setTimeout(() => this.innerView && this.innerView.docView.setSelection(0, 0, this.innerView.root, true), 0);
+ }
+
+ ignore = (e: PointerEvent) => {
+ e.stopPropagation();
+ document.removeEventListener("pointerup", this.ignore, true);
+ }
+
+ toggle = () => {
+ if (this.innerView) this.close();
+ else {
+ this.open();
+ }
+ }
+ close() {
+ 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);
+
+ 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));
+ }
+ }
+ if (outerTr.docChanged) this.outerView.dispatch(outerTr);
+ }
+ }
+ update(node: any) {
+ 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; }
+ this.innerView.dispatch(
+ state.tr
+ .replace(start, endB, node.slice(start, endA))
+ .setMeta("fromOutside", true));
+ }
+ }
+ return true;
+ }
+
+ destroy() {
+ if (this.innerView) this.close();
+ }
+
+ stopEvent(event: any) {
+ return this.innerView && this.innerView.dom.contains(event.target);
+ }
+
+ 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() { }
+
+ className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
- updateSummarizedText(start?: any, mark?: any) {
- let $start = this._view.state.doc.resolve(start);
+ 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.includes(_mark)) {
+ if (node.marks.find((m: any) => m.type === mark.type)) {
visited.add(node);
endPos = i + node.nodeSize - 1;
}
@@ -649,10 +871,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/TooltipLinkingMenu.tsx b/src/client/util/TooltipLinkingMenu.tsx
index 55e0eb909..e6d6c471f 100644
--- a/src/client/util/TooltipLinkingMenu.tsx
+++ b/src/client/util/TooltipLinkingMenu.tsx
@@ -1,23 +1,9 @@
-import { action, IReactionDisposer, reaction } from "mobx";
-import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css
-import { baseKeymap, lift } from "prosemirror-commands";
-import { history, redo, undo } from "prosemirror-history";
-import { keymap } from "prosemirror-keymap";
-import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";
+import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
-import { schema } from "./RichTextSchema";
-import { Schema, NodeType, MarkType } from "prosemirror-model";
-import React = require("react");
+import { FieldViewProps } from "../views/nodes/FieldView";
import "./TooltipTextMenu.scss";
+import React = require("react");
const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list';
-import {
- faListUl,
-} from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { FieldViewProps } from "../views/nodes/FieldView";
-import { throwStatement } from "babel-types";
const SVG = "http://www.w3.org/2000/svg";
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index eda5fe09c..34025787d 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -3,8 +3,8 @@ import { faListUl } from '@fortawesome/free-solid-svg-icons';
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 { liftListItem, wrapInList } from 'prosemirror-schema-list';
-import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
+import { wrapInList } from 'prosemirror-schema-list';
+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");
@@ -60,8 +61,6 @@ export class TooltipTextMenu {
@observable
private _storedMarks: Mark<any>[] | null | undefined;
- public HackToFixTextSelectionGlitch: boolean = false;
-
constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) {
this.view = view;
@@ -309,11 +308,10 @@ export class TooltipTextMenu {
{
handlers: {
dragComplete: action(() => {
- // let m = dragData.droppedDocuments;
let linkDoc = dragData.linkDocument;
let guid = Utils.GenerateGuid();
let proto = Doc.GetProto(linkDoc);
- if (docView && docView.props.ContainingCollectionView) {
+ if (proto && docView && docView.props.ContainingCollectionView) {
proto.sourceContext = docView.props.ContainingCollectionView.props.Document;
}
linkDoc.guid = guid;
@@ -326,8 +324,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);
}
@@ -436,11 +432,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;
}
@@ -500,10 +498,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);
@@ -513,30 +521,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 = "indent1";
- 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);
+ }
}
}
@@ -631,10 +646,9 @@ export class TooltipTextMenu {
if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
if (this._brushMarks && to - from > 0) {
this.view.dispatch(this.view.state.tr.removeMark(from, to));
- this._brushMarks.forEach((mark: Mark) => {
+ Array.from(this._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
const markType = mark.type;
this.changeToMarkInGroup(markType, this.view, []);
-
});
}
}
@@ -878,8 +892,6 @@ export class TooltipTextMenu {
this.updateFontSizeDropdown("Various");
}
}
- !this.HackToFixTextSelectionGlitch &&
- this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks)); // bcz: what's the purpose of this line? It messes up text selection without the Hack.
this.update_mark_doms();
}
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
new file mode 100644
index 000000000..188e3e1c5
--- /dev/null
+++ b/src/client/util/prosemirrorPatches.js
@@ -0,0 +1,139 @@
+'use strict';
+
+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.
+function liftListItem(itemType) {
+ return function (tx, dispatch) {
+ var ref = tx.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 }
+ if (!dispatch) { return true }
+ if ($from.node(range.depth - 1).type == itemType) // Inside a parent list
+ { return liftToOuterList(tx, dispatch, itemType, range) }
+ else // Outer list node
+ { return liftOutOfList(tx, dispatch, range) }
+ }
+}
+
+function liftToOuterList(tr, dispatch, itemType, range) {
+ var end = range.end, endOfList = range.$to.end(range.depth);
+ if (end < endOfList) {
+ // There are siblings after the lifted items, which must become
+ // children of the last item
+ tr.step(new prosemirrorTransform.ReplaceAroundStep(end - 1, endOfList, end, endOfList,
+ new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true));
+ range = new prosemirrorModel.NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
+ }
+ dispatch(tr.lift(range, prosemirrorTransform.liftTarget(range)).scrollIntoView());
+ return true
+}
+
+function liftOutOfList(tr, dispatch, range) {
+ var list = range.parent;
+ // Merge the list items into a single big item
+ for (var pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
+ pos -= list.child(i).nodeSize;
+ tr.delete(pos - 1, pos + 1);
+ }
+ var $start = tr.doc.resolve(range.start), item = $start.nodeAfter;
+ var atStart = range.startIndex == 0, atEnd = range.endIndex == list.childCount;
+ var parent = $start.node(-1), indexBefore = $start.index(-1);
+ if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1,
+ item.content.append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list)))) { return false }
+ var start = $start.pos, end = start + item.nodeSize;
+ // Strip off the surrounding list. At the sides where we're not at
+ // the end of the list, the existing list is closed. At sides where
+ // this is the end, it is overwritten to its end.
+ tr.step(new prosemirrorTransform.ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1,
+ new prosemirrorModel.Slice((atStart ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty)))
+ .append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty))),
+ 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..0366a6a30 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -89,7 +89,7 @@ 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 (
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 203227241..94aab8b2f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -20,7 +20,7 @@ import { DocumentView, PositionDocument } from "./nodes/DocumentView";
import { FieldView } from "./nodes/FieldView";
import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox";
import { IconBox } from "./nodes/IconBox";
-import { LinkMenu } from "./nodes/LinkMenu";
+import { LinkMenu } from "./linking/LinkMenu";
import { TemplateMenu } from "./TemplateMenu";
import { Template, Templates } from "./Templates";
import React = require("react");
@@ -428,6 +428,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();
}
@@ -615,6 +621,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;
@@ -818,6 +829,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let linkButton = null;
if (SelectionManager.SelectedDocuments().length > 0) {
let selFirst = SelectionManager.SelectedDocuments()[0];
+
let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length;
linkButton = (<Flyout
anchorPoint={anchorPoints.RIGHT_TOP}
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 3f40642b5..aa573f16b 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,35 @@ 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 +92,6 @@ export class InkingControl extends React.Component {
});
}
});
-
@action
switchWidth = (width: string): void => {
this._selectedWidth = width;
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 755e5de14..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;
@@ -48,11 +47,6 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
MainOverlayTextBox.Instance = this;
reaction(() => FormattedTextBox.InputBoxOverlay,
(box?: FormattedTextBox) => {
- const tb = this._textBox;
- const container = tb && tb.props.ContainingCollectionView;
- if (tb && container) {
- tb.rebuildEditor();// this forces the edited text box to completely recreate itself to avoid get out of sync -- e.g., bullet labels are only computed when the dom elements are created
- }
this._textBox = box;
if (box) {
this.ChromeHeight = box.props.ChromeHeight;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 6b856443b..b64986084 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,5 @@
import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv } from '@fortawesome/free-solid-svg-icons';
+import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -12,7 +12,7 @@ import { List } from '../../new_fields/List';
import { Id } from '../../new_fields/FieldSymbols';
import { InkTool } from '../../new_fields/InkField';
import { listSpec } from '../../new_fields/Schema';
-import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types';
+import { BoolCast, Cast, FieldValue, StrCast, NumCast } from '../../new_fields/Types';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { RouteStore } from '../../server/RouteStore';
import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils';
@@ -40,6 +40,8 @@ import { PreviewCursor } from './PreviewCursor';
import { FilterBox } from './search/FilterBox';
import PresModeMenu from './presentationview/PresentationModeMenu';
import { PresBox } from './nodes/PresBox';
+import { LinkFollowBox } from './linking/LinkFollowBox';
+import { DocumentManager } from '../util/DocumentManager';
@observer
export class MainView extends React.Component {
@@ -153,6 +155,7 @@ export class MainView extends React.Component {
constructor(props: Readonly<{}>) {
super(props);
MainView.Instance = this;
+
// causes errors to be generated when modifying an observable outside of an action
configure({ enforceActions: "observed" });
if (window.location.pathname !== RouteStore.home) {
@@ -431,10 +434,17 @@ export class MainView extends React.Component {
}
}
+ toggleLinkFollowBox = (shouldClose: boolean) => {
+ if (LinkFollowBox.Instance) {
+ let dvs = DocumentManager.Instance.getDocumentViews(LinkFollowBox.Instance.props.Document);
+ // if it already exisits, close it
+ LinkFollowBox.Instance.props.Document.isMinimized = (dvs.length > 0 && shouldClose);
+ }
+ }
+
@observable private _colorPickerDisplay = false;
/* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
nodesMenu() {
-
let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
@@ -474,6 +484,7 @@ export class MainView extends React.Component {
<FontAwesomeIcon icon={btn[1]} size="sm" />
</button>
</div></li>)}
+ <li key="linkFollow"><button className="add-button round-button" title="Open Link Follower" onClick={() => this.toggleLinkFollowBox(true)}><FontAwesomeIcon icon="link" size="sm" /></button></li>
<li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
<div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
<SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index dc122497f..26c2e0e1e 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -39,4 +39,9 @@
width: 10px;
height: 10px;
cursor: nwse-resize;
+}
+
+.overlayView-doc {
+ z-index: 1;
+ position: absolute;
} \ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 538614089..da4b71e5c 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -1,7 +1,7 @@
import * as React from "react";
import { observer } from "mobx-react";
-import { observable, action } from "mobx";
-import { Utils, emptyFunction, returnOne, returnTrue, returnEmptyString } from "../../Utils";
+import { observable, action, trace, computed } from "mobx";
+import { Utils, emptyFunction, returnOne, returnTrue, returnEmptyString, returnZero, returnFalse } from "../../Utils";
import './OverlayView.scss';
import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
@@ -10,6 +10,8 @@ import { Id } from "../../new_fields/FieldSymbols";
import { DocumentView } from "./nodes/DocumentView";
import { Transform } from "../util/Transform";
import { CollectionFreeFormDocumentView } from "./nodes/CollectionFreeFormDocumentView";
+import { DocumentContentsView } from "./nodes/DocumentContentsView";
+import { NumCast } from "../../new_fields/Types";
export type OverlayDisposer = () => void;
@@ -138,30 +140,67 @@ export class OverlayView extends React.Component {
return remove;
}
+ @computed get overlayDocs() {
+ return CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => {
+ let offsetx = 0, offsety = 0;
+ let onPointerMove = action((e: PointerEvent) => {
+ if (e.buttons === 1) {
+ d.x = e.clientX + offsetx;
+ d.y = e.clientY + offsety;
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ });
+ let onPointerUp = action((e: PointerEvent) => {
+ document.removeEventListener("pointermove", onPointerMove);
+ document.removeEventListener("pointerup", onPointerUp);
+ e.stopPropagation();
+ e.preventDefault();
+ });
+
+ let onPointerDown = (e: React.PointerEvent) => {
+ offsetx = NumCast(d.x) - e.clientX;
+ offsety = NumCast(d.y) - e.clientY;
+ e.stopPropagation();
+ e.preventDefault();
+ document.addEventListener("pointermove", onPointerMove);
+ document.addEventListener("pointerup", onPointerUp);
+ };
+ return <div className="overlayView-doc" key={d[Id]} onPointerDown={onPointerDown} style={{ transform: `translate(${d.x}px, ${d.y}px)`, display: d.isMinimized ? "none" : "" }}>
+ <DocumentContentsView
+ Document={d}
+ ChromeHeight={returnZero}
+ isSelected={returnFalse}
+ select={emptyFunction}
+ layoutKey={"layout"}
+ bringToFront={emptyFunction}
+ addDocument={undefined}
+ removeDocument={undefined}
+ ContentScaling={returnOne}
+ PanelWidth={returnOne}
+ PanelHeight={returnOne}
+ ScreenToLocalTransform={Transform.Identity}
+ renderDepth={1}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ addDocTab={emptyFunction}
+ pinToPres={emptyFunction}
+ ContainingCollectionView={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne} />
+ </div>;
+ });
+ }
+
render() {
return (
- <div className="overlayView">
- {this._elements}
- {CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => (
- <CollectionFreeFormDocumentView key={d[Id]}
- Document={d}
- bringToFront={emptyFunction}
- addDocument={undefined}
- removeDocument={undefined}
- ContentScaling={returnOne}
- PanelWidth={returnOne}
- PanelHeight={returnOne}
- ScreenToLocalTransform={Transform.Identity}
- renderDepth={1}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- focus={emptyFunction}
- backgroundColor={returnEmptyString}
- addDocTab={emptyFunction}
- pinToPres={emptyFunction}
- ContainingCollectionView={undefined}
- zoomToScale={emptyFunction}
- getScale={returnOne} />))}
+ <div className="overlayView" id="overlayView">
+ <div>
+ {this._elements}
+ </div>
+ {this.overlayDocs}
</div>
);
}
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 1329dc02c..45a8556bf 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,7 +101,7 @@ 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) {
+ !e.defaultPrevented) {
if (!e.ctrlKey && !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..7afba5e01 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -66,13 +66,24 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
</div>
);
}
- public static EditClickScript(doc: Doc, fieldKey: string) {
+ public static EditClickScript(doc: Doc, fieldKey: string, 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) => {
+ if (prewrapper) {
+ text = prewrapper + text + (postwrapper ? postwrapper : "");
+ }
const script = CompileScript(text, {
params: { this: Doc.name },
typecheck: false,
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
deleted file mode 100644
index fd4b2420d..000000000
--- a/src/client/views/SearchItem.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React = require("react");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Doc } from "../../new_fields/Doc";
-import { DocumentManager } from "../util/DocumentManager";
-import { SetupDrag } from "../util/DragManager";
-
-
-export interface SearchProps {
- doc: Doc;
-}
-
-library.add(faCaretUp);
-library.add(faObjectGroup);
-library.add(faStickyNote);
-library.add(faFilePdf);
-library.add(faFilm);
-
-export class SearchItem extends React.Component<SearchProps> {
-
- onClick = () => {
- DocumentManager.Instance.jumpToDocument(this.props.doc, false);
- }
-
- //needs help
- // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
-
-
- public static DocumentIcon(layout: string) {
- let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
- layout.indexOf("ImageBox") !== -1 ? faImage :
- layout.indexOf("Formatted") !== -1 ? faStickyNote :
- layout.indexOf("Video") !== -1 ? faFilm :
- layout.indexOf("Collection") !== -1 ? faObjectGroup :
- faCaretUp;
- return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
- }
- onPointerEnter = (e: React.PointerEvent) => {
- Doc.BrushDoc(this.props.doc);
- }
- onPointerLeave = (e: React.PointerEvent) => {
- Doc.UnBrushDoc(this.props.doc);
- }
-
- collectionRef = React.createRef<HTMLDivElement>();
- startDocDrag = () => {
- let doc = this.props.doc;
- const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
- if (isProto) {
- return Doc.MakeDelegate(doc);
- } else {
- return Doc.MakeAlias(doc);
- }
- }
- render() {
- return (
- <div className="search-item" ref={this.collectionRef} id="result"
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
- onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} >
- <div className="search-title" id="result" >title: {this.props.doc.title}</div>
- {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */}
- {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */}
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index b6ed6aaa0..bd8d56851 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,
@@ -103,6 +104,9 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer
Doc.GetProto(doc).annotationOn = this.props.Document;
}
+ if (doc.type === DocumentType.BUTTON) {
+ doc.collectionContext = this.props.Document; // used by docList() function in Doc.ts so that buttons can iterate over the documents in their collection
+ }
allowDuplicates = true;
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;
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/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 97b31bf2a..654ff2279 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 }}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 2d94f1b8e..a593128be 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -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 a7acd9e91..21f119d57 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";
@@ -152,6 +152,7 @@ export namespace PivotView {
y={pos.y}
width={pos.width}
height={pos.height}
+ jitterRotation={NumCast(target.props.Document.jitterRotation)}
{...target.getChildDocumentViewProps(doc)}
/>,
bounds: {
@@ -224,8 +225,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,
@@ -251,12 +256,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)]);
+ 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[]) => {
@@ -324,7 +351,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) {
@@ -384,6 +411,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return false;
}
@observable sets: (Doc[])[] = [];
+
+ @undoBatch
@action
updateClusters() {
this.sets.length = 0;
@@ -412,12 +441,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"];
@@ -742,7 +801,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 });
@@ -765,6 +826,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) }
@@ -869,6 +931,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"
});
@@ -879,9 +942,21 @@ 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[] = [];
+ noteItems.push({ description: "1: Note", event: () => this.createText("Note", "yellow"), icon: "eye" });
+ noteItems.push({ description: "2: Idea", event: () => this.createText("Idea", "pink"), icon: "eye" });
+ noteItems.push({ description: "3: Topic", event: () => this.createText("Topic", "lightBlue"), icon: "eye" });
+ noteItems.push({ description: "4: Person", event: () => this.createText("Person", "lightGreen"), icon: "eye" });
+ layoutItems.push({ description: "Add Note ...", subitems: noteItems, icon: "eye" })
ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" });
}
+ createText = (noteStyle: string, color: string) => {
+ let pt = this.getTransform().transformPoint(ContextMenu.Instance.pageX, ContextMenu.Instance.pageY);
+ this.addLiveTextBox(Docs.Create.TextDocument({ title: noteStyle, x: pt[0], y: pt[1], autoHeight: true, backgroundColor: color }))
+ }
private childViews = () => [
<CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
@@ -920,6 +995,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..56d8127e2 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,8 @@ 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";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -93,9 +95,8 @@ 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-" }));
}
e.stopPropagation();
}
@@ -273,14 +274,30 @@ 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);
+ }
+ });
+ 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.get("white") || usedPaletted.get("rgb(255,255,255)") && usedPaletted.size === 1 ? "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 +347,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/nodes/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss
index fc5f2410c..fc5f2410c 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/linking/LinkEditor.scss
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index ecb3e9db4..ecb3e9db4 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
diff --git a/src/client/views/linking/LinkFollowBox.scss b/src/client/views/linking/LinkFollowBox.scss
new file mode 100644
index 000000000..9eeed1cc8
--- /dev/null
+++ b/src/client/views/linking/LinkFollowBox.scss
@@ -0,0 +1,93 @@
+@import "../globalCssVariables";
+
+.linkFollowBox-main {
+ position: absolute;
+ background: whitesmoke;
+ color: grey;
+ border-radius: 15px;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ border: solid #BBBBBBBB 5px;
+ pointer-events: all;
+
+ .linkFollowBox-header {
+ height: 50px;
+ text-align: center;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 16px;
+ width: 100%;
+ }
+
+ .direction-indicator {
+ font-size: 12px;
+ }
+
+ .closeDocument {
+ position: relative;
+ max-width: 30px;
+ top: -20px;
+ left: 460px;
+ color: $darker-alt-accent
+ }
+
+ .closeDocument:hover {
+ color: $main-accent;
+ }
+
+ .topHeader {
+ width: 100%;
+ height: 25px;
+ }
+
+ .linkFollowBox-footer {
+ height: 50px;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ button {
+ background-color: $darker-alt-accent;
+ width: 30%;
+ }
+ }
+
+ .linkFollowBox-content {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-column-gap: 5px;
+ margin-left: 5px;
+ margin-right: 5px;
+
+ .linkFollowBox-item {
+ background-color: $light-color;
+ width: 100%;
+ height: 100%;
+
+ .linkFollowBox-itemContent {
+ padding: 5px;
+ font-size: 12px;
+ overflow: scroll;
+
+ input[type=radio] {
+ border: 0px;
+ margin-right: 5px;
+ }
+ }
+
+ .title {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-transform: uppercase;
+ color: $light-color;
+ background-color: $lighter-alt-accent;
+ width: 100%;
+ height: 30px;
+ border-bottom: solid $darker-alt-accent 5px;
+ font-size: 12px;
+ text-align: center;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
new file mode 100644
index 000000000..d5ed01f53
--- /dev/null
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -0,0 +1,611 @@
+import { observable, computed, action, runInAction, reaction, IReactionDisposer } from "mobx";
+import React = require("react");
+import { observer } from "mobx-react";
+import { FieldViewProps, FieldView } from "../nodes/FieldView";
+import { Doc, DocListCastAsync } from "../../../new_fields/Doc";
+import { undoBatch } from "../../util/UndoManager";
+import { NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types";
+import { CollectionViewType } from "../collections/CollectionBaseView";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { SelectionManager } from "../../util/SelectionManager";
+import { DocumentManager } from "../../util/DocumentManager";
+import { DocumentView } from "../nodes/DocumentView";
+import "./LinkFollowBox.scss";
+import { SearchUtil } from "../../util/SearchUtil";
+import { Id } from "../../../new_fields/FieldSymbols";
+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 { docs_v1 } from "googleapis";
+
+enum FollowModes {
+ OPENTAB = "Open in Tab",
+ OPENRIGHT = "Open in Right Split",
+ OPENFULL = "Open Full Screen",
+ PAN = "Pan to Document",
+ INPLACE = "Open In Place"
+}
+
+enum FollowOptions {
+ ZOOM = "Zoom",
+ NOZOOM = "No Zoom",
+}
+
+@observer
+export class LinkFollowBox extends React.Component<FieldViewProps> {
+
+ public static LayoutString() { return FieldView.LayoutString(LinkFollowBox); }
+ public static Instance: LinkFollowBox | undefined;
+ @observable static linkDoc: Doc | undefined = undefined;
+ @observable static destinationDoc: Doc | undefined = undefined;
+ @observable static sourceDoc: Doc | undefined = undefined;
+ @observable selectedMode: string = "";
+ @observable selectedContext: Doc | undefined = undefined;
+ @observable selectedContextAliases: Doc[] | undefined = undefined;
+ @observable selectedOption: string = "";
+ @observable selectedContextString: string = "";
+ @observable sourceView: DocumentView | undefined = undefined;
+ @observable canPan: boolean = false;
+ @observable shouldUseOnlyParentContext = false;
+ _contextDisposer?: IReactionDisposer;
+
+ @observable private _docs: { col: Doc, target: Doc }[] = [];
+ @observable private _otherDocs: { col: Doc, target: Doc }[] = [];
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ LinkFollowBox.Instance = this;
+ this.resetVars();
+ this.props.Document.isBackground = true;
+ }
+
+ componentDidMount = () => {
+ this.resetVars();
+
+ this._contextDisposer = reaction(
+ () => this.selectedContextString,
+ async () => {
+ let ref = await DocServer.GetRefField(this.selectedContextString);
+ runInAction(() => {
+ if (ref instanceof Doc) {
+ this.selectedContext = ref;
+ }
+ });
+ if (this.selectedContext instanceof Doc) {
+ let aliases = await SearchUtil.GetViewsOfDocument(this.selectedContext);
+ runInAction(() => { this.selectedContextAliases = aliases; });
+ }
+ }
+ );
+ }
+
+ componentWillUnmount = () => {
+ this._contextDisposer && this._contextDisposer();
+ }
+
+ async resetPan() {
+ if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionView) {
+ let colDoc = this.sourceView.props.ContainingCollectionView.props.Document;
+ runInAction(() => { this.canPan = false; });
+ if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) {
+ let docs = Cast(colDoc.data, listSpec(Doc), []);
+ let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc));
+
+ aliases.forEach(alias => {
+ if (docs.filter(doc => doc === alias).length > 0) {
+ runInAction(() => { this.canPan = true; });
+ }
+ });
+ }
+ }
+ }
+
+ @action
+ resetVars = () => {
+ this.selectedContext = undefined;
+ this.selectedContextString = "";
+ this.selectedMode = "";
+ this.selectedOption = "";
+ LinkFollowBox.linkDoc = undefined;
+ LinkFollowBox.sourceDoc = undefined;
+ LinkFollowBox.destinationDoc = undefined;
+ this.sourceView = undefined;
+ this.canPan = false;
+ this.shouldUseOnlyParentContext = false;
+ }
+
+ async fetchDocuments() {
+ if (LinkFollowBox.destinationDoc) {
+ let dest: Doc = LinkFollowBox.destinationDoc;
+ let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(dest));
+ const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${dest[Id]}"` });
+ const map: Map<Doc, Doc> = new Map;
+ const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs)));
+ allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
+ docs.forEach(doc => map.delete(doc));
+ runInAction(async () => {
+ this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: dest }));
+ this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+ let tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.targetContext, Doc)) as Doc;
+ runInAction(() => tcontext && this._docs.splice(0, 0, { col: tcontext, target: dest }));
+ });
+ }
+ }
+
+ @action
+ setLinkDocs = (linkDoc: Doc, source: Doc, dest: Doc) => {
+ this.resetVars();
+
+ LinkFollowBox.linkDoc = linkDoc;
+ LinkFollowBox.sourceDoc = source;
+ LinkFollowBox.destinationDoc = dest;
+ this.fetchDocuments();
+
+ SelectionManager.SelectedDocuments().forEach(dv => {
+ if (dv.props.Document === LinkFollowBox.sourceDoc) {
+ this.sourceView = dv;
+ }
+ });
+
+ this.resetPan();
+ }
+
+ unhighlight = () => {
+ Doc.UnhighlightAll();
+ document.removeEventListener("pointerdown", this.unhighlight);
+ }
+
+ @action
+ highlightDoc = () => {
+ if (LinkFollowBox.destinationDoc) {
+ document.removeEventListener("pointerdown", this.unhighlight);
+ Doc.HighlightDoc(LinkFollowBox.destinationDoc);
+ window.setTimeout(() => {
+ document.addEventListener("pointerdown", this.unhighlight);
+ }, 10000);
+ }
+ }
+
+ @undoBatch
+ openFullScreen = () => {
+ if (LinkFollowBox.destinationDoc) {
+ let view: DocumentView | null = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
+ view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
+ SelectionManager.DeselectAll();
+ }
+ }
+
+ @undoBatch
+ 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;
+ options.context.panX = newPanX;
+ options.context.panY = newPanY;
+ }
+ let view = DocumentManager.Instance.getDocumentView(options.context);
+ view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+ }
+
+ // should container be a doc or documentview or what? This one needs work and is more long term
+ @undoBatch
+ openInContainer = (options: { container: Doc }) => {
+
+ }
+
+ _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;
+ options.context.panX = newPanX;
+ options.context.panY = newPanY;
+ }
+ CollectionDockingView.Instance.AddRightSplit(options.context, undefined);
+
+ if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
+
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+ }
+
+ @undoBatch
+ openLinkRight = () => {
+ if (LinkFollowBox.destinationDoc) {
+ let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
+ CollectionDockingView.Instance.AddRightSplit(alias, undefined);
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+
+ }
+
+ @undoBatch
+ jumpToLink = async (options: { shouldZoom: boolean }) => {
+ if (LinkFollowBox.destinationDoc && LinkFollowBox.linkDoc) {
+ let jumpToDoc: Doc = LinkFollowBox.destinationDoc;
+ let pdfDoc = FieldValue(Cast(LinkFollowBox.destinationDoc, Doc));
+ if (pdfDoc) {
+ jumpToDoc = pdfDoc;
+ }
+ 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._addDocTab && this._addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
+
+ if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 && 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, shouldZoom, false, document => dockingFunc(sourceContext!));
+ }
+ else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, undefined, undefined,
+ NumCast((LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 ? LinkFollowBox.linkDoc.anchor2Page : LinkFollowBox.linkDoc.anchor1Page)));
+
+ }
+ else {
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, dockingFunc);
+ }
+
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+ }
+
+ @undoBatch
+ openLinkTab = () => {
+ if (LinkFollowBox.destinationDoc) {
+ let fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
+ // this.prosp.addDocTab is empty -- use the link source's addDocTab
+ this._addDocTab && this._addDocTab(fullScreenAlias, undefined, "inTab");
+
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+ }
+
+ @undoBatch
+ openLinkColTab = (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;
+ options.context.panX = newPanX;
+ options.context.panY = newPanY;
+ }
+ this._addDocTab && this._addDocTab(options.context, undefined, "inTab");
+ if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
+
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+ }
+
+ @undoBatch
+ openLinkInPlace = (options: { shouldZoom: boolean }) => {
+
+ if (LinkFollowBox.destinationDoc && LinkFollowBox.sourceDoc) {
+ let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
+ let y = NumCast(LinkFollowBox.sourceDoc.y);
+ let x = NumCast(LinkFollowBox.sourceDoc.x);
+
+ let width = NumCast(LinkFollowBox.sourceDoc.width);
+ let height = NumCast(LinkFollowBox.sourceDoc.height);
+
+ alias.x = x + width + 30;
+ alias.y = y;
+ alias.width = width;
+ alias.height = height;
+
+ if (this.sourceView && this.sourceView.props.addDocument) {
+ this.sourceView.props.addDocument(alias, false);
+ }
+
+ this.jumpToLink({ shouldZoom: options.shouldZoom });
+
+ this.highlightDoc();
+ SelectionManager.DeselectAll();
+ }
+ }
+
+ //set this to be the default link behavior, can be any of the above
+ public defaultLinkBehavior: (options?: any) => void = this.jumpToLink;
+
+ @action
+ currentLinkBehavior = () => {
+ // this.resetPan();
+ if (LinkFollowBox.destinationDoc) {
+ if (this.selectedContextString === "") {
+ this.selectedContextString = "self";
+ this.selectedContext = LinkFollowBox.destinationDoc;
+ }
+ if (this.selectedOption === "") this.selectedOption = FollowOptions.NOZOOM;
+ let shouldZoom: boolean = this.selectedOption === FollowOptions.NOZOOM ? false : true;
+ let notOpenInContext: boolean = this.selectedContextString === "self" || this.selectedContextString === LinkFollowBox.destinationDoc[Id];
+
+ if (this.selectedMode === FollowModes.INPLACE) {
+ if (shouldZoom !== undefined) this.openLinkInPlace({ shouldZoom: shouldZoom });
+ }
+ else if (this.selectedMode === FollowModes.OPENFULL) {
+ if (notOpenInContext) this.openFullScreen();
+ else this.selectedContext && this.openColFullScreen({ context: this.selectedContext });
+ }
+ else if (this.selectedMode === FollowModes.OPENRIGHT) {
+ if (notOpenInContext) this.openLinkRight();
+ else this.selectedContext && this.openLinkColRight({ context: this.selectedContext, shouldZoom: shouldZoom });
+ }
+ else if (this.selectedMode === FollowModes.OPENTAB) {
+ if (notOpenInContext) this.openLinkTab();
+ else this.selectedContext && this.openLinkColTab({ context: this.selectedContext, shouldZoom: shouldZoom });
+ }
+ else if (this.selectedMode === FollowModes.PAN) {
+ this.jumpToLink({ shouldZoom: shouldZoom });
+ }
+ else return;
+ }
+ }
+
+ @action
+ handleModeChange = (e: React.ChangeEvent) => {
+ let target = e.target as HTMLInputElement;
+ this.selectedMode = target.value;
+ this.selectedContext = undefined;
+ this.selectedContextString = "";
+
+ this.shouldUseOnlyParentContext = (this.selectedMode === FollowModes.INPLACE || this.selectedMode === FollowModes.PAN);
+
+ if (this.shouldUseOnlyParentContext) {
+ if (this.sourceView && this.sourceView.props.ContainingCollectionView) {
+ this.selectedContext = this.sourceView.props.ContainingCollectionView.props.Document;
+ this.selectedContextString = (StrCast(this.sourceView.props.ContainingCollectionView.props.Document.title));
+ }
+ }
+ }
+
+ @action
+ handleOptionChange = (e: React.ChangeEvent) => {
+ let target = e.target as HTMLInputElement;
+ this.selectedOption = target.value;
+ }
+
+ @action
+ handleContextChange = (e: React.ChangeEvent) => {
+ let target = e.target as HTMLInputElement;
+ this.selectedContextString = target.value;
+ // selectedContext is updated in reaction
+ this.selectedOption = "";
+ }
+
+ @computed
+ get canOpenInPlace() {
+ if (this.sourceView && this.sourceView.props.ContainingCollectionView) {
+ let colView = this.sourceView.props.ContainingCollectionView;
+ let colDoc = colView.props.Document;
+ if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) return true;
+ }
+ return false;
+ }
+
+ @computed
+ get availableModes() {
+ return (
+ <div>
+ <label><input
+ type="radio"
+ name="mode"
+ value={FollowModes.OPENRIGHT}
+ checked={this.selectedMode === FollowModes.OPENRIGHT}
+ onChange={this.handleModeChange}
+ disabled={false} />
+ {FollowModes.OPENRIGHT}
+ </label><br />
+ <label><input
+ type="radio"
+ name="mode"
+ value={FollowModes.OPENTAB}
+ checked={this.selectedMode === FollowModes.OPENTAB}
+ onChange={this.handleModeChange}
+ disabled={false} />
+ {FollowModes.OPENTAB}
+ </label><br />
+ <label><input
+ type="radio"
+ name="mode"
+ value={FollowModes.OPENFULL}
+ checked={this.selectedMode === FollowModes.OPENFULL}
+ onChange={this.handleModeChange}
+ disabled={false} />
+ {FollowModes.OPENFULL}
+ </label><br />
+ <label><input
+ type="radio"
+ name="mode"
+ value={FollowModes.PAN}
+ checked={this.selectedMode === FollowModes.PAN}
+ onChange={this.handleModeChange}
+ disabled={!this.canPan} />
+ {FollowModes.PAN}
+ </label><br />
+ <label><input
+ type="radio"
+ name="mode"
+ value={FollowModes.INPLACE}
+ checked={this.selectedMode === FollowModes.INPLACE}
+ onChange={this.handleModeChange}
+ disabled={!this.canOpenInPlace} />
+ {FollowModes.INPLACE}
+ </label><br />
+ </div>
+ );
+ }
+
+ @computed
+ get parentName() {
+ if (this.sourceView && this.sourceView.props.ContainingCollectionView) {
+ let colView = this.sourceView.props.ContainingCollectionView;
+ return colView.props.Document.title;
+ }
+ }
+
+ @computed
+ get parentID(): string {
+ if (this.sourceView && this.sourceView.props.ContainingCollectionView) {
+ let colView = this.sourceView.props.ContainingCollectionView;
+ return StrCast(colView.props.Document[Id]);
+ }
+ return "col";
+ }
+
+ @computed
+ get availableContexts() {
+ return (
+ this.shouldUseOnlyParentContext ?
+ <label><input
+ type="radio" disabled={true}
+ name="context"
+ value={this.parentID}
+ checked={true} />
+ {this.parentName} (Parent Collection)
+ </label>
+ :
+ <div>
+ <label><input
+ type="radio" disabled={LinkFollowBox.linkDoc ? false : true}
+ name="context"
+ value={LinkFollowBox.destinationDoc ? StrCast(LinkFollowBox.destinationDoc[Id]) : "self"}
+ checked={LinkFollowBox.destinationDoc ? this.selectedContextString === StrCast(LinkFollowBox.destinationDoc[Id]) || this.selectedContextString === "self" : true}
+ onChange={this.handleContextChange} />
+ Open Self
+ </label><br />
+ {[...this._docs, ...this._otherDocs].map(doc => {
+ if (doc && doc.target && doc.col.title !== "Recently Closed") {
+ return <div key={doc.col[Id] + doc.target[Id]}><label key={doc.col[Id] + doc.target[Id]}>
+ <input
+ type="radio" disabled={LinkFollowBox.linkDoc ? false : true}
+ name="context"
+ value={StrCast(doc.col[Id])}
+ checked={this.selectedContextString === StrCast(doc.col[Id])}
+ onChange={this.handleContextChange} />
+ {doc.col.title}
+ </label><br /></div>;
+ }
+ })}
+ </div>
+ );
+ }
+
+ @computed
+ get shouldShowZoom(): boolean {
+ if (this.selectedMode === FollowModes.OPENFULL) return false;
+ if (this.shouldUseOnlyParentContext) return true;
+ if (LinkFollowBox.destinationDoc ? this.selectedContextString === LinkFollowBox.destinationDoc[Id] : "self") return false;
+
+ let contextMatch: boolean = false;
+ if (this.selectedContextAliases) {
+ this.selectedContextAliases.forEach(alias => {
+ if (alias.viewType === CollectionViewType.Freeform) contextMatch = true;
+ });
+ }
+ if (contextMatch) return true;
+
+ return false;
+ }
+
+ @computed
+ get availableOptions() {
+ if (LinkFollowBox.destinationDoc) {
+ return (
+ this.shouldShowZoom ?
+ <div>
+ <label><input
+ type="radio"
+ name="option"
+ value={FollowOptions.ZOOM}
+ checked={this.selectedOption === FollowOptions.ZOOM}
+ onChange={this.handleOptionChange}
+ disabled={false} />
+ {FollowOptions.ZOOM}
+ </label><br />
+ <label><input
+ type="radio"
+ name="option"
+ value={FollowOptions.NOZOOM}
+ checked={this.selectedOption === FollowOptions.NOZOOM}
+ onChange={this.handleOptionChange}
+ disabled={false} />
+ {FollowOptions.NOZOOM}
+ </label><br />
+ </div>
+ :
+ <div>No Available Options</div>
+ );
+ }
+ return null;
+ }
+
+ render() {
+ return (
+ <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document.height), width: NumCast(this.props.Document.width) }}>
+ <div className="linkFollowBox-header">
+ <div className="topHeader">
+ {LinkFollowBox.linkDoc ? "Link Title: " + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"}
+ <div onClick={() => this.props.Document.isMinimized = true} className="closeDocument"><FontAwesomeIcon icon={faTimes} size="lg" /></div>
+ </div>
+ <div className=" direction-indicator">{LinkFollowBox.linkDoc ?
+ LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc ? "Source: " + StrCast(LinkFollowBox.sourceDoc.title) + ", Destination: " + StrCast(LinkFollowBox.destinationDoc.title)
+ : "" : ""}</div>
+ </div>
+ <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document.height) - 110 }}>
+ <div className="linkFollowBox-item">
+ <div className="linkFollowBox-item title">Mode</div>
+ <div className="linkFollowBox-itemContent">
+ {LinkFollowBox.linkDoc ? this.availableModes : "Please select a link to view modes"}
+ </div>
+ </div>
+ <div className="linkFollowBox-item">
+ <div className="linkFollowBox-item title">Context</div>
+ <div className="linkFollowBox-itemContent">
+ {this.selectedMode !== "" ? this.availableContexts : "Please select a mode to view contexts"}
+ </div>
+ </div>
+ <div className="linkFollowBox-item">
+ <div className="linkFollowBox-item title">Options</div>
+ <div className="linkFollowBox-itemContent">
+ {this.selectedContextString !== "" ? this.availableOptions : "Please select a context to view options"}
+ </div>
+ </div>
+ </div>
+ <div className="linkFollowBox-footer">
+ <button
+ onClick={this.resetVars}>
+ Clear Link
+ </button>
+ <div style={{ width: 20 }}></div>
+ <button
+ onClick={this.currentLinkBehavior}
+ disabled={(LinkFollowBox.linkDoc) ? false : true}>
+ Follow Link
+ </button>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss
index a4018bd2d..a4018bd2d 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/linking/LinkMenu.scss
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 1908889e9..842ce45b1 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -1,6 +1,6 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { DocumentView } from "./DocumentView";
+import { DocumentView } from "../nodes/DocumentView";
import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
import React = require("react");
@@ -39,6 +39,7 @@ export class LinkMenu extends React.Component<Props> {
linkItems.push(
<LinkMenuGroup
key={groupType}
+ docView={this.props.docView}
sourceDoc={this.props.docView.props.Document}
group={group}
groupType={groupType}
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index e04044266..b6a24b0c8 100644
--- a/src/client/views/nodes/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -1,6 +1,6 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { DocumentView } from "./DocumentView";
+import { DocumentView } from "../nodes/DocumentView";
import { LinkMenuItem } from "./LinkMenuItem";
import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
@@ -22,6 +22,8 @@ interface LinkMenuGroupProps {
groupType: string;
showEditor: (linkDoc: Doc) => void;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ docView: DocumentView;
+
}
@observer
@@ -83,9 +85,13 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
let groupItems = this.props.group.map(linkDoc => {
let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
if (destination && this.props.sourceDoc) {
- return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType}
+ return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]}
+ groupType={this.props.groupType}
addDocTab={this.props.addDocTab}
- linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />;
+ linkDoc={linkDoc}
+ sourceDoc={this.props.sourceDoc}
+ destinationDoc={destination}
+ showEditor={this.props.showEditor} />;
}
});
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 15f8c0786..0e951fc38 100644
--- a/src/client/views/nodes/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -1,11 +1,18 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } 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 { Doc } from '../../../new_fields/Doc';
+import { Cast, StrCast } from '../../../new_fields/Types';
+import { DragLinkAsDocument } from '../../util/DragManager';
+import { LinkManager } from '../../util/LinkManager';
+import { ContextMenu } from '../ContextMenu';
+import { MainView } from '../MainView';
+import { LinkFollowBox } from './LinkFollowBox';
import './LinkMenu.scss';
import React = require("react");
+<<<<<<< HEAD:src/client/views/nodes/LinkMenuItem.tsx
import { Doc, DocListCastAsync } from '../../../new_fields/Doc';
import { StrCast, Cast, FieldValue, NumCast } from '../../../new_fields/Types';
import { observable, action } from 'mobx';
@@ -15,6 +22,8 @@ import { CollectionDockingView } from '../collections/CollectionDockingView';
import { SelectionManager } from '../../util/SelectionManager';
import { Utils } from '../../../Utils';
import { Id } from '../../../new_fields/FieldSymbols';
+=======
+>>>>>>> ec62b213439ab49134fa2dbbdf38a6d1ef5737cd:src/client/views/linking/LinkMenuItem.tsx
library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
@@ -31,6 +40,7 @@ interface LinkMenuItemProps {
export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
private _drag = React.createRef<HTMLDivElement>();
@observable private _showMore: boolean = false;
+<<<<<<< HEAD:src/client/views/nodes/LinkMenuItem.tsx
@action toggleShowMore() { this._showMore = !this._showMore; }
@undoBatch
@@ -76,11 +86,16 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
else {
DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, dockingFunc);
}
+=======
+ @action toggleShowMore() {
+ this._showMore = !this._showMore;
+>>>>>>> ec62b213439ab49134fa2dbbdf38a6d1ef5737cd:src/client/views/linking/LinkMenuItem.tsx
}
onEdit = (e: React.PointerEvent): void => {
e.stopPropagation();
this.props.showEditor(this.props.linkDoc);
+ //SelectionManager.DeselectAll();
}
renderMetadata = (): JSX.Element => {
@@ -113,6 +128,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();
}
@@ -126,6 +147,30 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
e.stopPropagation();
}
+ onContextMenu = (e: React.MouseEvent) => {
+ e.preventDefault();
+ ContextMenu.Instance.addItem({ description: "Open in Link Follower", event: () => this.openLinkFollower(), icon: "link" });
+ ContextMenu.Instance.addItem({ description: "Follow Default Link", event: () => this.followDefault(), icon: "arrow-right" });
+ ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
+ }
+
+ @action.bound
+ async followDefault() {
+ if (LinkFollowBox.Instance !== undefined) {
+ LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
+ LinkFollowBox.Instance.defaultLinkBehavior();
+ }
+ }
+
+ @action.bound
+ async openLinkFollower() {
+ if (LinkFollowBox.Instance !== undefined) {
+ LinkFollowBox.Instance.props.Document.isMinimized = false;
+ MainView.Instance.toggleLinkFollowBox(false);
+ LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
+ }
+ }
+
render() {
let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
@@ -140,7 +185,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
{canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}>
<FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>}
<div title="Edit link" className="button" onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
- <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
+ <div title="Follow link" className="button" onClick={this.followDefault} onContextMenu={this.onContextMenu}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
</div>
</div>
{this._showMore ? this.renderMetadata() : <></>}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 7631ecc6c..9692dd8a9 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,20 +1,21 @@
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 { returnEmptyString } from "../../../Utils";
+import { random } from "animejs";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
x?: number;
y?: number;
width?: number;
height?: number;
+ jitterRotation: number;
}
const schema = createSchema({
@@ -28,7 +29,7 @@ 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 transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg) 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; }
@@ -76,6 +77,21 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
clusterColorFunc = (doc: Doc) => this.clusterColor;
render() {
+ let txf = this.transform;
+ let w = this.width;
+ let h = this.height;
+ let renderScript = this.Document.renderScript;
+ if (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;
+ w = NumCast(someView.width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width);
+ h = NumCast(someView.height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height);
+ txf = `translate(${x}px,${y}px)`;
+ }
+ }
const hasPosition = this.props.x !== undefined || this.props.y !== undefined;
return (
<div className="collectionFreeFormDocumentView-container"
@@ -83,15 +99,18 @@ 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.props.Document.opacity === 0 ? undefined : // if it's not visible, then no shadow
+ this.props.Document.z ? `#9c9396 ${StrCast(this.props.Document.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow
+ this.clusterColor ? (
+ this.props.Document.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` : // if it's a background & has a cluster color, make the shadow spread really big
+ `${this.clusterColor} ${StrCast(this.props.Document.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent
+ undefined,
borderRadius: this.borderRounding(),
- transform: this.transform,
+ transform: txf,
transition: hasPosition ? "transform 1s" : StrCast(this.props.Document.transition),
- width: this.width,
- height: this.height,
+ width: w,
+ height: h,
zIndex: this.Document.zIndex || 0,
}} >
<DocumentView {...this.props}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index d77662355..d0e117fe4 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -14,6 +14,7 @@ import { ImageBox } from "./ImageBox";
import { DragBox } from "./DragBox";
import { ButtonBox } from "./ButtonBox";
import { PresBox } from "./PresBox";
+import { LinkFollowBox } from "../linking/LinkFollowBox";
import { IconBox } from "./IconBox";
import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
@@ -30,6 +31,7 @@ import { List } from "../../../new_fields/List";
import { Doc } from "../../../new_fields/Doc";
import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
import { ScriptField } from "../../../new_fields/ScriptField";
+import { fromPromise } from "mobx-utils";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -108,7 +110,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
return <ObserverJsxParser
blacklistedAttrs={[]}
- components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox }}
+ components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox }}
bindings={this.CreateBindings()}
jsx={this.finalLayout}
showWarnings={true}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4d5307c88..633455f63 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -375,8 +375,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);
}
@@ -593,11 +602,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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: "OnClick foreach doc", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onClick", "docList(this.collectionContext.data).map(d => {", "});\n") });
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();
+ 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;
+ }
}, 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" });
@@ -758,19 +773,19 @@ 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);
let borderRounding = StrCast(Doc.GetProto(this.props.Document).borderRounding);
- let localScale = this.props.ScreenToLocalTransform().Scale * brushDegree;
+ let localScale = this.props.ScreenToLocalTransform().Scale * fullDegree;
return (
<div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all",
color: foregroundColor,
- outlineColor: ["transparent", "maroon", "maroon"][brushDegree],
- outlineStyle: ["none", "dashed", "solid"][brushDegree],
- outlineWidth: brushDegree && !borderRounding ? `${localScale}px` : "0px",
- border: brushDegree && borderRounding ? `${["none", "dashed", "solid"][brushDegree]} ${["transparent", "maroon", "maroon"][brushDegree]} ${localScale}px` : undefined,
+ outlineColor: ["transparent", "maroon", "maroon", "yellow"][fullDegree],
+ outlineStyle: ["none", "dashed", "solid", "solid"][fullDegree],
+ outlineWidth: fullDegree && !borderRounding ? `${localScale}px` : "0px",
+ border: fullDegree && borderRounding ? `${["none", "dashed", "solid", "solid"][fullDegree]} ${["transparent", "maroon", "maroon", "yellow"][fullDegree]} ${localScale}px` : undefined,
borderRadius: "inherit",
background: backgroundColor,
width: nativeWidth,
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 03e81bfca..d7ac7a9c5 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -84,18 +84,94 @@
align-content: center;
}
-ol { counter-reset: deci 0;}
-.decimal-ol { counter-reset: deci 0; p { display: inline }; font-size: 24 }
+footnote {
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ div {
+ padding : 0 !important;
+ }
+}
+
+footnote::after {
+ content: counter(prosemirror-footnote);
+ vertical-align: super;
+ font-size: 75%;
+ counter-increment: prosemirror-footnote;
+}
+
+.ProseMirror {
+ counter-reset: prosemirror-footnote;
+ }
+
+.footnote-tooltip {
+ cursor: auto;
+ font-size: 75%;
+ position: absolute;
+ left: -30px;
+ top: calc(100% + 10px);
+ background: silver;
+ padding: 3px;
+ border-radius: 2px;
+ max-width: 100px;
+ min-width: 50px;
+ width: max-content;
+}
+
+.prosemirror-attribution {
+ font-size: 8px;
+}
+.footnote-tooltip::before {
+ border: 5px solid silver;
+ border-top-width: 0px;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ position: absolute;
+ top: -5px;
+ left: 27px;
+ content: " ";
+ height: 0;
+ 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 }
.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 }
.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 }
+.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 }
+.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 }
+.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 }
.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18 }
.lower-roman-ol {counter-reset: lroman; p { display: inline }; font-size: 14; }
.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10;}
-.decimal:before { content: counter(deci) " "; counter-increment: deci; display:inline-block; width: 30}
-.decimal2:before { content: counter(deci) "." counter(deci2) " "; counter-increment: deci2; display:inline-block; width: 35}
-.decimal3:before { content: counter(deci) "." counter(deci2) "." counter(deci3) " "; counter-increment: deci3; display:inline-block; width: 35}
-.decimal4:before { content: counter(deci) "." counter(deci2) "." counter(deci3) "." counter(deci4) " "; counter-increment: deci4; display:inline-block; width: 40}
-.upper-alpha:before { content: counter(deci) "." counter(ualph, upper-alpha) " "; counter-increment: ualph; display:inline-block; width: 35 }
-.lower-roman:before { content: counter(deci) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) " "; counter-increment: lroman;display:inline-block; width: 50 }
-.lower-alpha:before { content: counter(deci) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha)" "; counter-increment: lalpha; display:inline-block; width: 35}
+.decimal1:before { content: counter(deci1) ")"; counter-increment: deci1; display:inline-block; width: 30}
+.decimal2:before { content: counter(deci1) "." counter(deci2) ")"; counter-increment: deci2; display:inline-block; width: 35}
+.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ")"; counter-increment: deci3; display:inline-block; width: 35}
+.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ")"; counter-increment: deci4; display:inline-block; width: 40}
+.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ")"; counter-increment: deci5; display:inline-block; width: 40}
+.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ")"; counter-increment: deci6; display:inline-block; width: 45}
+.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ")"; counter-increment: deci7; display:inline-block; width: 50}
+.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ")"; counter-increment: ualph; display:inline-block; width: 35 }
+.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ")"; counter-increment: lroman;display:inline-block; width: 50 }
+.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ")"; counter-increment: lalpha; display:inline-block; width: 35}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index ec1ddbdb7..985075a52 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -13,16 +13,16 @@ import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc";
import { Copy, Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
import { RichTextField, ToPlainText, FromPlainText } 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';
import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorExampleTransfer";
import { inpRules } from "../../util/RichTextRules";
-import { ImageResizeView, schema, SummarizedView } from "../../util/RichTextSchema";
+import { ImageResizeView, schema, SummarizedView, OrderedListView, FootnoteView } from "../../util/RichTextSchema";
import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
@@ -37,9 +37,9 @@ import { DocumentDecorations } from '../DocumentDecorations';
import { DictationManager } from '../../util/DictationManager';
import { ReplaceStep } from 'prosemirror-transform';
import { DocumentType } from '../../documents/DocumentTypes';
-import { link } from 'fs';
-import { selectionSizePlugin, findStartOfMark, findUserMark, findEndOfMark, findOtherUserMark, SelectionSizeTooltip } from './FormattedTextBoxComment';
-import { date } from 'serializr';
+import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
+import { inputRules } from 'prosemirror-inputrules';
+import { select } from 'async';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
@@ -77,6 +77,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 +119,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;
@@ -201,40 +203,62 @@ 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);
+ if (desc !== view.lastSelectedViewDesc) {
+ if (view.lastSelectedViewDesc) {
+ view.lastSelectedViewDesc.deselectNode();
+ view.lastSelectedViewDesc = null;
+ }
+ if (desc) { desc.selectNode(); }
+ view.lastSelectedViewDesc = desc;
+ }
+ } else {
+ if (view.lastSelectedViewDesc) {
+ view.lastSelectedViewDesc.deselectNode();
+ view.lastSelectedViewDesc = null;
+ }
}
}
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);
- FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = true);
this._editorView.updateState(state);
- FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = false);
+ 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
if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
}
@@ -244,12 +268,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 ? "..." : "");
}
}
@@ -292,7 +320,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();
@@ -304,10 +337,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];
@@ -372,14 +408,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({
@@ -387,23 +425,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
attributes: { class: "ProseMirror-example-setup-style" }
}
}),
- selectionSizePlugin
+ 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(),
() => {
@@ -623,9 +654,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (link) {
cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
linkId = link[Id];
- let guid = StrCast(link.guid);
- link.guid = guid;
- 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);
@@ -636,24 +665,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);
}
}
@@ -671,32 +696,68 @@ 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(); },
+ footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); }
},
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
+ (this._editorView as any).isOverlay = this.props.isOverlay;
if (startup) {
Doc.GetProto(doc).documentText = undefined;
this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
}
}
- 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() {
@@ -707,19 +768,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) {
@@ -732,6 +792,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];
@@ -757,7 +824,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"));
}
}
});
@@ -780,36 +848,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
onPointerUp = (e: React.PointerEvent): void => {
- let view = this._editorView!;
- const pos = view.posAtCoords({ left: e.clientX, top: e.clientY });
- const rpos = pos && view.state.doc.resolve(pos.pos);
- let noselection = view.state.selection.$from === view.state.selection.$to;
- let set = false;
- if (pos && rpos) {
- let nbef = findStartOfMark(rpos, view, findOtherUserMark);
- let naft = findEndOfMark(rpos, view, findOtherUserMark);
- const spos = pos.pos - nbef;
- const epos = pos.pos + naft;
- let child = rpos.nodeBefore || rpos.nodeAfter;
- let mark = child && findOtherUserMark(child.marks);
- if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
- SelectionSizeTooltip.SetState(this, mark.attrs.opened, spos, epos, mark);
- set = true;
- }
- }
- !set && SelectionSizeTooltip.Hide();
+ FormattedTextBoxComment.textBox = this;
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
}
- 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);
@@ -831,6 +875,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 = "";
@@ -874,44 +929,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();
- setTimeout(() => { // force re-rendering of bullet numbers that may have had their bullet labels change. (Our prosemirrior code re-"marks" the changed bullets, but nothing causes the Dom to be re-rendered which is where the nubering takes place)
- SelectionManager.DeselectAll();
- SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(this.props.Document, this.props.ContainingCollectionView)!, false);
- }, 0);
- }
- 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;
- if (s < 10) s = '0' + s;
- return now.toLocaleDateString() + ' ' + h + ':' + m + ':' + s + ' ' + 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
@@ -928,7 +953,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 9123f8aed..2d2fff06d 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -1,18 +1,25 @@
-import { Plugin, EditorState } from "prosemirror-state"
-import './FormattedTextBoxComment.scss'
+import { Plugin, EditorState } from "prosemirror-state";
+import './FormattedTextBoxComment.scss';
import { ResolvedPos, Mark } from "prosemirror-model";
import { EditorView } from "prosemirror-view";
import { Doc } from "../../../new_fields/Doc";
+import { schema } from "../../util/RichTextSchema";
+import { DocServer } from "../../DocServer";
+import { Utils } from "../../../Utils";
+import { StrCast } from "../../../new_fields/Types";
-export let selectionSizePlugin = new Plugin({
- view(editorView) { return new SelectionSizeTooltip(editorView); }
-})
+export let formattedTextBoxCommentPlugin = new Plugin({
+ view(editorView) { return new FormattedTextBoxComment(editorView); }
+});
export function findOtherUserMark(marks: Mark[]): Mark | undefined {
return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
}
export function findUserMark(marks: Mark[]): Mark | undefined {
return marks.find(m => m.attrs.userid);
}
+export function findLinkMark(marks: Mark[]): Mark | undefined {
+ return marks.find(m => m.type === schema.marks.link);
+}
export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
let before = 0;
let nbef = rpos.nodeBefore;
@@ -35,7 +42,7 @@ export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (mark
}
-export class SelectionSizeTooltip {
+export class FormattedTextBoxComment {
static tooltip: HTMLElement;
static tooltipText: HTMLElement;
static start: number;
@@ -44,39 +51,39 @@ export class SelectionSizeTooltip {
static opened: boolean;
static textBox: any;
constructor(view: any) {
- if (!SelectionSizeTooltip.tooltip) {
+ if (!FormattedTextBoxComment.tooltip) {
const root = document.getElementById("root");
let input = document.createElement("input");
input.type = "checkbox";
- SelectionSizeTooltip.tooltip = document.createElement("div");
- SelectionSizeTooltip.tooltipText = document.createElement("div");
- SelectionSizeTooltip.tooltip.appendChild(SelectionSizeTooltip.tooltipText);
- SelectionSizeTooltip.tooltip.className = "FormattedTextBox-tooltip";
- SelectionSizeTooltip.tooltip.style.pointerEvents = "all";
- SelectionSizeTooltip.tooltip.appendChild(input);
- SelectionSizeTooltip.tooltip.onpointerdown = (e: PointerEvent) => {
+ FormattedTextBoxComment.tooltip = document.createElement("div");
+ FormattedTextBoxComment.tooltipText = document.createElement("div");
+ FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText);
+ FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip";
+ FormattedTextBoxComment.tooltip.style.pointerEvents = "all";
+ FormattedTextBoxComment.tooltip.appendChild(input);
+ FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
let keep = e.target && (e.target as any).type === "checkbox";
- SelectionSizeTooltip.opened = keep || !SelectionSizeTooltip.opened;
- SelectionSizeTooltip.textBox.setAnnotation(
- SelectionSizeTooltip.start, SelectionSizeTooltip.end, SelectionSizeTooltip.mark,
- SelectionSizeTooltip.opened, keep);
+ FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened;
+ FormattedTextBoxComment.textBox && FormattedTextBoxComment.textBox.setAnnotation(
+ FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark,
+ FormattedTextBoxComment.opened, keep);
};
- root && root.appendChild(SelectionSizeTooltip.tooltip);
+ root && root.appendChild(FormattedTextBoxComment.tooltip);
}
this.update(view, undefined);
}
public static Hide() {
- SelectionSizeTooltip.textBox = undefined;
- SelectionSizeTooltip.tooltip && (SelectionSizeTooltip.tooltip.style.display = "none");
+ FormattedTextBoxComment.textBox = undefined;
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none");
}
public static SetState(textBox: any, opened: boolean, start: number, end: number, mark: Mark) {
- SelectionSizeTooltip.textBox = textBox;
- SelectionSizeTooltip.start = start;
- SelectionSizeTooltip.end = end;
- SelectionSizeTooltip.mark = mark;
- SelectionSizeTooltip.opened = opened;
- SelectionSizeTooltip.textBox && SelectionSizeTooltip.tooltip && (SelectionSizeTooltip.tooltip.style.display = "");
+ FormattedTextBoxComment.textBox = textBox;
+ FormattedTextBoxComment.start = start;
+ FormattedTextBoxComment.end = end;
+ FormattedTextBoxComment.mark = mark;
+ FormattedTextBoxComment.opened = opened;
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "");
}
update(view: EditorView, lastState?: EditorState) {
@@ -85,13 +92,46 @@ export class SelectionSizeTooltip {
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)) return;
+ let set = "none";
if (state.selection.$from) {
let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
+ const spos = state.selection.$from.pos - nbef;
+ const epos = state.selection.$from.pos + naft;
let child = state.selection.$from.nodeBefore;
let mark = child && findOtherUserMark(child.marks);
+ let noselection = view.state.selection.$from === view.state.selection.$to;
+ if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
+ FormattedTextBoxComment.SetState(this, mark.attrs.opened, spos, epos, mark);
+ }
+ if (mark && child && nbef && naft) {
+ FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " " + mark.attrs.modified;
+ // These are in screen coordinates
+ // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
+ let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
+ // The box in which the tooltip is positioned, to use as base
+ let box = (document.getElementById("main-div") as any).getBoundingClientRect();
+ // Find a center-ish x position from the selection endpoints (when
+ // crossing lines, end may be more to the left)
+ let left = Math.max((start.left + end.left) / 2, start.left + 3);
+ FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
+ FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
+ set = "";
+ }
+ }
+ if (set === "none" && state.selection.$from) {
+ FormattedTextBoxComment.textBox = undefined;
+ let nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
+ let naft = findEndOfMark(state.selection.$from, view, findLinkMark);
+ let child = state.selection.$from.nodeBefore;
+ let mark = child && findLinkMark(child.marks);
if (mark && child && nbef && naft) {
- SelectionSizeTooltip.tooltipText.textContent = mark.attrs.userid + " " + mark.attrs.modified;
+ FormattedTextBoxComment.tooltipText.textContent = "link : " + (mark.attrs.title || mark.attrs.href);
+ 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)));
+ }
// These are in screen coordinates
// let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
@@ -100,11 +140,13 @@ export class SelectionSizeTooltip {
// Find a center-ish x position from the selection endpoints (when
// crossing lines, end may be more to the left)
let left = Math.max((start.left + end.left) / 2, start.left + 3);
- SelectionSizeTooltip.tooltip.style.left = (left - box.left) + "px";
- SelectionSizeTooltip.tooltip.style.bottom = (box.bottom - start.top) + "px";
+ FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
+ FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
+ set = "";
}
}
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set);
}
- destroy() { SelectionSizeTooltip.tooltip.style.display = "none" }
+ destroy() { FormattedTextBoxComment.tooltip.style.display = "none"; }
}
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/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 642f58daf..29eef27a0 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,22 +1,27 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { FieldResult, Doc } from "../../../new_fields/Doc";
+import { FieldResult, Doc, Field } from "../../../new_fields/Doc";
import { HtmlField } from "../../../new_fields/HtmlField";
-import { InkTool } from "../../../new_fields/InkField";
-import { Cast, NumCast } from "../../../new_fields/Types";
import { WebField } from "../../../new_fields/URLField";
-import { Utils } from "../../../Utils";
import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from './FieldView';
-import { KeyValueBox } from "./KeyValueBox";
import "./WebBox.scss";
import React = require("react");
+import { InkTool } from "../../../new_fields/InkField";
+import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { Utils } from "../../../Utils";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { observable, action, computed } from "mobx";
+import { listSpec } from "../../../new_fields/Schema";
+import { RefField } from "../../../new_fields/RefField";
+import { ObjectField } from "../../../new_fields/ObjectField";
+import { updateSourceFile } from "typescript";
+import { KeyValueBox } from "./KeyValueBox";
+import { setReactionScheduler } from "mobx/lib/internal";
+import { library } from "@fortawesome/fontawesome-svg-core";
import { SelectionManager } from "../../util/SelectionManager";
import { Docs } from "../../documents/Documents";
-import { faStickyNote } from "@fortawesome/free-solid-svg-icons";
-import { library } from "@fortawesome/fontawesome-svg-core";
library.add(faStickyNote);
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 429642e9f..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(1 as any);
+ 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/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 672892fdf..386b5fe74 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -63,6 +63,7 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
runInAction(() => {
this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.doc }));
this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+
});
}