aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAubrey Li <Aubrey-Li>2021-10-26 17:16:16 -0400
committerAubrey Li <Aubrey-Li>2021-10-26 17:16:16 -0400
commit34ce1ba0275406aff180a49f99d333ffa0d86e3b (patch)
tree25e8ae29b145e5e1e33c59e285f3b29f5a481dc5
parent3c1b393732ef9dc704a2f40b103c37b3f8370ba7 (diff)
parent48d5e650ddc8caa8252561bbc91961f2f4677d6e (diff)
Merge branch 'master' into trails-aubrey
-rw-r--r--package-lock.json20
-rw-r--r--package.json4
-rw-r--r--src/Utils.ts17
-rw-r--r--src/client/documents/Documents.ts50
-rw-r--r--src/client/goldenLayout.js12
-rw-r--r--src/client/util/CurrentUserUtils.ts51
-rw-r--r--src/client/util/DocumentManager.ts7
-rw-r--r--src/client/util/DragManager.ts15
-rw-r--r--src/client/util/InteractionUtils.tsx149
-rw-r--r--src/client/util/LinkManager.ts33
-rw-r--r--src/client/util/SelectionManager.ts14
-rw-r--r--src/client/util/SettingsManager.tsx19
-rw-r--r--src/client/util/SharingManager.scss1
-rw-r--r--src/client/util/UndoManager.ts19
-rw-r--r--src/client/views/AntimodeMenu.tsx2
-rw-r--r--src/client/views/ContextMenu.scss1
-rw-r--r--src/client/views/ContextMenu.tsx20
-rw-r--r--src/client/views/DocumentButtonBar.tsx7
-rw-r--r--src/client/views/DocumentDecorations.scss28
-rw-r--r--src/client/views/DocumentDecorations.tsx120
-rw-r--r--src/client/views/GestureOverlay.tsx18
-rw-r--r--src/client/views/GlobalKeyHandler.ts18
-rw-r--r--src/client/views/InkControlPtHandles.tsx176
-rw-r--r--src/client/views/InkControls.tsx148
-rw-r--r--src/client/views/InkHandles.tsx124
-rw-r--r--src/client/views/InkStroke.scss3
-rw-r--r--src/client/views/InkStrokeProperties.ts280
-rw-r--r--src/client/views/InkTangentHandles.tsx131
-rw-r--r--src/client/views/InkingStroke.tsx167
-rw-r--r--src/client/views/LightboxView.tsx4
-rw-r--r--src/client/views/MainView.scss38
-rw-r--r--src/client/views/MainView.tsx45
-rw-r--r--src/client/views/MarqueeAnnotator.tsx6
-rw-r--r--src/client/views/PreviewCursor.scss5
-rw-r--r--src/client/views/PreviewCursor.tsx4
-rw-r--r--src/client/views/SidebarAnnos.tsx2
-rw-r--r--src/client/views/StyleProvider.tsx31
-rw-r--r--src/client/views/collections/CollectionDockingView.scss23
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx575
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx5
-rw-r--r--src/client/views/collections/CollectionSubView.tsx7
-rw-r--r--src/client/views/collections/CollectionView.tsx12
-rw-r--r--src/client/views/collections/TabDocView.tsx3
-rw-r--r--src/client/views/collections/TreeView.tsx21
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx3
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx27
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx35
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx40
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx3
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx1
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx87
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx78
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx18
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.scss10
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx9
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTable.tsx16
-rw-r--r--src/client/views/global/globalCssVariables.scss1
-rw-r--r--src/client/views/linking/LinkEditor.tsx35
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx1
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx22
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx5
-rw-r--r--src/client/views/nodes/ComparisonBox.scss1
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx22
-rw-r--r--src/client/views/nodes/DocumentView.scss1
-rw-r--r--src/client/views/nodes/DocumentView.tsx69
-rw-r--r--src/client/views/nodes/EquationBox.scss3
-rw-r--r--src/client/views/nodes/EquationBox.tsx8
-rw-r--r--src/client/views/nodes/FilterBox.tsx6
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx14
-rw-r--r--src/client/views/nodes/ImageBox.scss2
-rw-r--r--src/client/views/nodes/ImageBox.tsx11
-rw-r--r--src/client/views/nodes/LabelBigText.js241
-rw-r--r--src/client/views/nodes/LabelBox.scss8
-rw-r--r--src/client/views/nodes/LabelBox.tsx35
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx2
-rw-r--r--src/client/views/nodes/LinkBox.tsx19
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.tsx7
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx5
-rw-r--r--src/client/views/nodes/PDFBox.tsx32
-rw-r--r--src/client/views/nodes/VideoBox.scss15
-rw-r--r--src/client/views/nodes/VideoBox.tsx26
-rw-r--r--src/client/views/nodes/WebBox.scss86
-rw-r--r--src/client/views/nodes/WebBox.tsx207
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx229
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx62
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.scss2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx56
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx194
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts4
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts6
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx42
-rw-r--r--src/client/views/pdf/Annotation.scss3
-rw-r--r--src/client/views/pdf/Annotation.tsx5
-rw-r--r--src/client/views/pdf/PDFViewer.tsx41
-rw-r--r--src/client/views/search/SearchBox.tsx15
-rw-r--r--src/fields/Doc.ts15
-rw-r--r--src/fields/Proxy.ts2
-rw-r--r--src/fields/RichTextUtils.ts4
-rw-r--r--src/fields/util.ts25
-rw-r--r--src/mobile/MobileInterface.tsx4
-rw-r--r--src/server/DashUploadUtils.ts4
-rw-r--r--src/server/server_Initialization.ts113
102 files changed, 2380 insertions, 2092 deletions
diff --git a/package-lock.json b/package-lock.json
index 5fdc3352c..288247869 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -666,6 +666,11 @@
"integrity": "sha1-TN2WtJKTs5MhIuS34pVD415rrlg=",
"dev": true
},
+ "@types/bezier-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@types/bezier-js/-/bezier-js-4.1.0.tgz",
+ "integrity": "sha512-ElU16s8E6Pr6magp8ihwH1O8pbUJASbMND/qgUc9RsLmP3lMLHiDMRXdjtaObwW5GPtOVYOsXDUIhTIluT+yaw=="
+ },
"@types/bluebird": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.32.tgz",
@@ -2678,6 +2683,11 @@
"resolved": "https://registry.npmjs.org/bezier-curve/-/bezier-curve-1.0.0.tgz",
"integrity": "sha1-o9+v6rEqlMRicw1QeYxSqEBdc3k="
},
+ "bezier-js": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-4.1.1.tgz",
+ "integrity": "sha512-oVOS6SSFFFlfnZdzC+lsfvhs/RRcbxJ47U04M4s5QIBaJmr3YWmTIL3qmrOK9uW+nUUcl9Jccmo/xpTrG+bBoQ=="
+ },
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -6132,6 +6142,11 @@
}
}
},
+ "exifr": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
+ "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="
+ },
"expand-brackets": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -9312,6 +9327,11 @@
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"optional": true
},
+ "memorystream": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+ "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI="
+ },
"meow": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
diff --git a/package.json b/package.json
index e21e67368..b0e74440c 100644
--- a/package.json
+++ b/package.json
@@ -127,6 +127,7 @@
"@material-ui/core": "^4.11.0",
"@react-google-maps/api": "^2.2.0",
"@react-three/fiber": "^6.0.16",
+ "@types/bezier-js": "^4.1.0",
"@types/cors": "^2.8.8",
"@types/d3-axis": "^2.0.0",
"@types/d3-color": "^2.0.1",
@@ -146,6 +147,7 @@
"babel-runtime": "^6.26.0",
"bcrypt-nodejs": "0.0.3",
"bezier-curve": "^1.0.0",
+ "bezier-js": "^4.1.1",
"bluebird": "^3.7.2",
"body-parser": "^1.18.3",
"bootstrap": "^4.5.0",
@@ -163,6 +165,7 @@
"depcheck": "^0.9.2",
"equation-editor-react": "github:bobzel/equation-editor-react#useLocally",
"exif": "^0.6.0",
+ "exifr": "^7.1.3",
"express": "^4.16.4",
"express-flash": "0.0.2",
"express-session": "^1.17.0",
@@ -193,6 +196,7 @@
"libxmljs": "^0.19.7",
"lodash": "^4.17.15",
"material-ui": "^0.20.2",
+ "memorystream": "^0.3.1",
"mobile-detect": "^1.4.4",
"mobx": "^5.15.7",
"mobx-react": "^5.4.4",
diff --git a/src/Utils.ts b/src/Utils.ts
index 6eacd8296..bfb29fe8d 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -125,7 +125,9 @@ export namespace Utils {
// bcz: isTransparent(__value__) is a hack. it would be nice to have acual functions be parsed, but now Doc.matchFieldValue is hardwired to recognize just this one
return `backgroundColor:${isTransparentFunctionHack},${noRecursionHack}:x`;// bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
}
-
+ export function PropUnsetFilter(prop: string) {
+ return `${prop}:any,${noRecursionHack}:unset`;
+ }
export function toRGBAstr(col: { r: number, g: number, b: number, a?: number }) {
return "rgba(" + col.r + "," + col.g + "," + col.b + (col.a !== undefined ? "," + col.a : "") + ")";
@@ -571,10 +573,19 @@ export function simulateMouseClick(element: Element | null | undefined, x: numbe
}
}
+export function DashColor(color: string) {
+ try {
+ return color ? Color(color.toLowerCase()) : Color("transparent");
+ } catch (e) {
+ console.log("COLOR error:", e);
+ return Color("red");
+ }
+}
+
export function lightOrDark(color: any) {
const nonAlphaColor = color.startsWith("#") ? (color as string).substring(0, 7) :
color.startsWith("rgba") ? color.replace(/,.[^,]*\)/, ")").replace("rgba", "rgb") : color;
- const col = Color(nonAlphaColor).rgb();
+ const col = DashColor(nonAlphaColor).rgb();
const colsum = (col.red() + col.green() + col.blue());
if (colsum / col.alpha() > 400 || col.alpha() < 0.25) return Colors.DARK_GRAY;
else return Colors.WHITE;
@@ -588,7 +599,7 @@ export function getWordAtPoint(elem: any, x: number, y: number): string | undefi
range.selectNodeContents(elem);
var currentPos = 0;
const endPos = range.endOffset;
- while (currentPos + 1 < endPos) {
+ while (currentPos + 1 <= endPos) {
range.setStart(elem, currentPos);
range.setEnd(elem, currentPos + 1);
const rangeRect = range.getBoundingClientRect();
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 369876428..ca2926129 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -142,8 +142,8 @@ export class DocumentOptions {
_columnWidth?: number;
_columnsHideIfEmpty?: boolean; // whether stacking view column headings should be hidden
_fontSize?: string;
- _fontWeight?: number;
_fontFamily?: string;
+ _fontWeight?: string;
_pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views
_curPage?: number; // current page of a PDF or other? paginated document
_currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds
@@ -168,6 +168,7 @@ export class DocumentOptions {
version?: string; // version identifier for a document
label?: string;
hidden?: boolean;
+ _hidden?: boolean;
mediaState?: string; // status of media document: "pendingRecording", "recording", "paused", "playing"
autoPlayAnchors?: boolean; // whether to play audio/video when an anchor is clicked in a stackedTimeline.
dontPlayLinkOnSelect?: boolean; // whether an audio/video should start playing when a link is followed to it.
@@ -193,6 +194,7 @@ export class DocumentOptions {
opacity?: number;
defaultBackgroundColor?: string;
_isLinkButton?: boolean; // marks a document as a button that will follow its primary link when clicked
+ _linkAutoMove?: boolean; // whether link endpoint should move around the edges of a document to make shortest path to other link endpoint
isFolder?: boolean;
lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide)
activeFrame?: number; // the active frame of a document in a frame base collection
@@ -304,6 +306,7 @@ export class DocumentOptions {
border?: string; //for searchbox
hoverBackgroundColor?: string; // background color of a label when hovered
linkRelationshipList?: List<string>; // for storing different link relationships (when set by user in the link editor)
+ linkRelationshipSizes?: List<number>; //stores number of links contained in each relationship
linkColorList?: List<string>; // colors of links corresponding to specific link relationships
}
export namespace Docs {
@@ -355,7 +358,7 @@ export namespace Docs {
const TemplateMap: TemplateMap = new Map([
[DocumentType.RTF, {
layout: { view: FormattedTextBox, dataField: "text" },
- options: { _height: 150, _xMargin: 10, _yMargin: 10, links: ComputedField.MakeFunction("links(self)") as any }
+ options: { _height: 150, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: ComputedField.MakeFunction("links(self)") as any }
}],
[DocumentType.SEARCH, {
layout: { view: SearchBox, dataField: defaultDataKey },
@@ -375,7 +378,7 @@ export namespace Docs {
}],
[DocumentType.WEB, {
layout: { view: WebBox, dataField: defaultDataKey },
- options: { _height: 300, _fitWidth: true, links: ComputedField.MakeFunction("links(self)") as any }
+ options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: ComputedField.MakeFunction("links(self)") as any }
}],
[DocumentType.COL, {
layout: { view: CollectionView, dataField: defaultDataKey },
@@ -395,7 +398,7 @@ export namespace Docs {
}],
[DocumentType.PDF, {
layout: { view: PDFBox, dataField: defaultDataKey },
- options: { _curPage: 1, _fitWidth: true, links: ComputedField.MakeFunction("links(self)") as any }
+ options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: ComputedField.MakeFunction("links(self)") as any }
}],
[DocumentType.MAP, {
layout: { view: MapBox, dataField: defaultDataKey },
@@ -408,7 +411,7 @@ export namespace Docs {
[DocumentType.LINK, {
layout: { view: LinkBox, dataField: defaultDataKey },
options: {
- childDontRegisterViews: true, _isLinkButton: true, _height: 150, description: "",
+ childDontRegisterViews: true, _isLinkButton: true, _height: 150, description: "", showCaption: "description",
backgroundColor: "lightblue", // lightblue is default color for linking dot and link documents text comment area
links: ComputedField.MakeFunction("links(self)") as any,
_removeDropProperties: new List(["_layerTags", "isLinkButton"]),
@@ -437,7 +440,7 @@ export namespace Docs {
}],
[DocumentType.EQUATION, {
layout: { view: EquationBox, dataField: defaultDataKey },
- options: { links: ComputedField.MakeFunction("links(self)") as any }
+ options: { links: ComputedField.MakeFunction("links(self)") as any, hideResizeHandles: true, hideDecorationTitle: true }
}],
[DocumentType.FUNCPLOT, {
layout: { view: FunctionPlotBox, dataField: defaultDataKey },
@@ -470,9 +473,9 @@ export namespace Docs {
layout: { view: CollectionView, dataField: defaultDataKey },
options: { links: ComputedField.MakeFunction("links(self)") as any, hideLinkButton: true }
}],
- [DocumentType.INK, {
+ [DocumentType.INK, { // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method
layout: { view: InkingStroke, dataField: defaultDataKey },
- options: { _fontFamily: "cursive", backgroundColor: "transparent", links: ComputedField.MakeFunction("links(self)") as any }
+ options: { links: ComputedField.MakeFunction("links(self)") as any }
}],
[DocumentType.SCREENSHOT, {
layout: { view: ScreenshotBox, dataField: defaultDataKey },
@@ -513,6 +516,14 @@ export namespace Docs {
const prototypeIds = Object.values(DocumentType).filter(type => type !== DocumentType.NONE).map(type => type + suffix);
// fetch the actual prototype documents from the server
const actualProtos = Docs.newAccount ? {} : await DocServer.GetRefFields(prototypeIds);
+ if (!Docs.newAccount) {
+ Cast(actualProtos[DocumentType.WEB + suffix], Doc, null).nativeHeightUnfrozen = true; // to avoid having to recreate the DB
+ Cast(actualProtos[DocumentType.PDF + suffix], Doc, null).nativeHeightUnfrozen = true;
+ Cast(actualProtos[DocumentType.RTF + suffix], Doc, null).nativeHeightUnfrozen = true;
+ Cast(actualProtos[DocumentType.WEB + suffix], Doc, null).nativeDimModifiable = true; // to avoid having to recreate the DB
+ Cast(actualProtos[DocumentType.PDF + suffix], Doc, null).nativeDimModifiable = true;
+ Cast(actualProtos[DocumentType.RTF + suffix], Doc, null).nativeDimModifiable = true;
+ }
// update this object to include any default values: DocumentOptions for all prototypes
prototypeIds.map(id => {
@@ -723,6 +734,8 @@ export namespace Docs {
I.type = DocumentType.INK;
I.layout = InkingStroke.LayoutString("data");
I.color = color;
+ I.hideDecorationTitle = true; // don't show title when selected
+ I.hideOpenButton = true; // don't show open full screen button when selected
I.fillColor = fillColor;
I.strokeWidth = strokeWidth;
I.strokeBezier = strokeBezier;
@@ -875,7 +888,7 @@ export namespace Docs {
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
const tabs = TreeDocument(documents, { title: "On-Screen Tabs", childDontRegisterViews: true, freezeChildren: "remove|add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", _fitWidth: true, system: true, isFolder: true });
const all = TreeDocument([], { title: "Off-Screen Tabs", childDontRegisterViews: true, freezeChildren: "add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", system: true, isFolder: true });
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List([tabs, all]), { freezeChildren: "remove|add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List([tabs, all]), { freezeChildren: "remove|add", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id);
}
export function DirectoryImportDocument(options: DocumentOptions = {}) {
@@ -977,13 +990,17 @@ export namespace DocUtils {
// metadata facets that exist
const exists = Object.keys(facet).filter(value => facet[value] === "exists");
+ // metadata facets that exist
+ const unsets = Object.keys(facet).filter(value => facet[value] === "unset");
+
// facets that have an x next to them
const xs = Object.keys(facet).filter(value => facet[value] === "x");
- if (!exists.length && !xs.length && !checks.length && !matches.length) return true;
+ if (!unsets.length && !exists.length && !xs.length && !checks.length && !matches.length) return true;
const failsNotEqualFacets = !xs.length ? false : xs.some(value => Doc.matchFieldValue(d, facetKey, value));
const satisfiesCheckFacets = !checks.length ? true : checks.some(value => Doc.matchFieldValue(d, facetKey, value));
const satisfiesExistsFacets = !exists.length ? true : exists.some(value => d[facetKey] !== undefined);
+ const satisfiesUnsetsFacets = !unsets.length ? true : unsets.some(value => d[facetKey] === undefined);
const satisfiesMatchFacets = !matches.length ? true : matches.some(value => {
if (facetKey.startsWith("*")) { // fields starting with a '*' are used to match families of related fields. ie, *lastModified will match text-lastModified, data-lastModified, etc
const allKeys = Array.from(Object.keys(d));
@@ -995,11 +1012,11 @@ export namespace DocUtils {
});
// if we're ORing them together, the default return is false, and we return true for a doc if it satisfies any one set of criteria
if ((parentCollection?.currentFilter as Doc)?.filterBoolean === "OR") {
- if (satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true;
+ if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true;
}
// if we're ANDing them together, the default return is true, and we return false for a doc if it doesn't satisfy any set of criteria
else {
- if (!satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false;
+ if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false;
}
}
@@ -1103,10 +1120,12 @@ export namespace DocUtils {
"anchor2-useLinkSmallAnchor": target.doc.useLinkSmallAnchor ? true : undefined,
"acl-Public": SharingPermissions.Augment,
"_acl-Public": SharingPermissions.Augment,
- layout_linkView: Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null),
- linkDisplay: true, hidden: true,
+ linkDisplay: true,
+ _hidden: true,
+ _linkAutoMove: true,
linkRelationship,
- _layoutKey: "layout_linkView",
+ _showCaption: "description",
+ _showTitle: "linkRelationship",
description
}, id), showPopup);
}
@@ -1206,7 +1225,6 @@ export namespace DocUtils {
description: ":" + StrCast(note.title),
event: undoBatch((args: { x: number, y: number }) => {
const textDoc = Docs.Create.TextDocument("", {
- _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize),
_width: 200, x, y, _autoHeight: note._autoHeight !== false,
title: StrCast(note.title) + "#" + (note.aliasCount = NumCast(note.aliasCount) + 1)
});
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 238f1ac0a..896237e1d 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -1583,7 +1583,7 @@
close: 'close',
maximise: 'maximise',
minimise: 'minimise',
- popout: 'open in new window',
+ popout: 'new tab',
popin: 'pop in',
tabDropdown: 'additional tabs'
}
@@ -2355,6 +2355,7 @@
this.element.hide();
}
});
+
/**
* This class represents a header above a Stack ContentItem.
*
@@ -2362,6 +2363,7 @@
* @param {lm.item.AbstractContentItem} parent
*/
lm.controls.Header = function (layoutManager, parent) {
+
lm.utils.EventEmitter.call(this);
this.layoutManager = layoutManager;
@@ -4449,7 +4451,11 @@
lm.items.Stack = function (layoutManager, config, parent) {
lm.items.AbstractContentItem.call(this, layoutManager, config, parent);
- this.element = $('<div class="lm_item lm_stack"></div>');
+ this.element = $(
+ '<div class="lm_item lm_stack">'
+ + '<p class="empty-tabs-message">Click <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAUCAAAAABHICnvAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAHdElNRQfkCBsXMgbrEyzaAAAAT0lEQVQY02NgIAcIu8tgEW3/u4IDQ5B14/8LQlhFhckVFfCJjIyIOfP/QWpEZGSQJFS05s9fIPj3/z+YmseCTxS7CZS7DI+PsYcOjpAkDAA6H0KZxzDzlgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wOC0yN1QyMzo1MDowNi0wNDowMDvgVpQAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDgtMjdUMjM6NTA6MDYtMDQ6MDBKve4oAAAAAElFTkSuQmCC"/> to create a new tab</p>'
+ + '</div>'
+ );
this._activeContentItem = null;
var cfg = layoutManager.config;
this._header = { // defaults' reconstruction from old configuration style
@@ -5029,7 +5035,7 @@
'close',
'maximise',
'minimise',
- 'open in new window'
+ 'new tab'
];
};
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f40cae676..435d40d2a 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -20,6 +20,7 @@ import { Networking } from "../Network";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
+import { TreeView } from "../views/collections/TreeView";
import { Colors } from "../views/global/globalEnums";
import { MainView } from "../views/MainView";
import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox";
@@ -38,7 +39,6 @@ import { ColorScheme } from "./SettingsManager";
import { SharingManager } from "./SharingManager";
import { SnappingManager } from "./SnappingManager";
import { UndoManager } from "./UndoManager";
-import { TreeView } from "../views/collections/TreeView";
interface Button {
title?: string;
@@ -226,9 +226,9 @@ export class CurrentUserUtils {
const descriptionWrapper = MasonryDocument([details, short, long], { ...shared, ...descriptionWrapperOpts });
descriptionWrapper._columnHeaders = new List<SchemaHeaderField>([
- new SchemaHeaderField("[A Short Description]", "dimGray", undefined, undefined, undefined, false),
- new SchemaHeaderField("[Long Description]", "dimGray", undefined, undefined, undefined, true),
- new SchemaHeaderField("[Details]", "dimGray", undefined, undefined, undefined, true),
+ new SchemaHeaderField("[A Short Description]", "dimgray", undefined, undefined, undefined, false),
+ new SchemaHeaderField("[Long Description]", "dimgray", undefined, undefined, undefined, true),
+ new SchemaHeaderField("[Details]", "dimgray", undefined, undefined, undefined, true),
]);
const detailView = Docs.Create.StackingDocument([carousel, descriptionWrapper], { ...shared, ...detailViewOpts, _chromeHidden: true, system: true });
detailView.isTemplateDoc = makeTemplate(detailView);
@@ -349,7 +349,7 @@ export class CurrentUserUtils {
static setupDefaultIconTemplates(doc: Doc) {
if (doc["template-icon-view"] === undefined) {
const iconView = Docs.Create.LabelDocument({
- title: "icon", textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("title"), _backgroundColor: "dimGray",
+ title: "icon", textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("title"), _backgroundColor: "dimgray",
_width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)"), system: true
});
// Docs.Create.TextDocument("", {
@@ -815,22 +815,25 @@ export class CurrentUserUtils {
const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`);
const newDashboardButton: Doc = Docs.Create.FontIconDocument({ onClick: newDashboard, _forceActive: true, toolTip: "Create new dashboard", _stayInCollection: true, _hideContextMenu: true, title: "new dashboard", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true });
doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], {
- title: "My Dashboards", _showTitle: "title", _height: 400, childHideLinkButton: true,
+ title: "My Dashboards", _showTitle: "title", _height: 400, childHideLinkButton: true, freezeChildren: "remove|add",
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newDashboardButton,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true,
explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files."
}));
- // const toggleTheme = ScriptField.MakeScript(`Doc.UserDoc().darkScheme = !Doc.UserDoc().darkScheme`);
- // const toggleComic = ScriptField.MakeScript(`toggleComicMode()`);
- // const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`);
+ const toggleDarkTheme = ScriptField.MakeScript(`this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`);
+ const toggleComic = ScriptField.MakeScript(`toggleComicMode()`);
+ const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`);
const shareDashboard = ScriptField.MakeScript(`shareDashboard(self)`);
const removeDashboard = ScriptField.MakeScript('removeDashboard(self)');
- (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, shareDashboard!, removeDashboard!]);
- (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Share Dashboard", "Remove Dashboard"]);
- (doc.myDashboards as any as Doc).childContextMenuIcons = new List<string>(["plus", "user-friends", "times"]);
- // (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, toggleTheme!, toggleComic!, snapshotDashboard!, shareDashboard!, removeDashboard!]);
- // (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard"]);
+ const developerFilter = ScriptField.MakeFunction('!IsNoviceMode()');
+ // (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, shareDashboard!, removeDashboard!]);
+ // (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Share Dashboard", "Remove Dashboard"]);
+ // (doc.myDashboards as any as Doc).childContextMenuIcons = new List<string>(["plus", "user-friends", "times"]);
+ (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, toggleDarkTheme!, toggleComic!, snapshotDashboard!, shareDashboard!, removeDashboard!]);
+ (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Toggle Dark Theme", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard"]);
+ (doc.myDashboards as any as Doc).childContextMenuIcons = new List<string>(["plus", "chalkboard", "tv", "camera", "users", "times"]);
+ (doc.myDashboards as any as Doc).childContextMenuFilters = new List<ScriptField>([undefined as any, developerFilter, developerFilter, developerFilter, undefined as any, undefined as any]);
}
return doc.myDashboards as any as Doc;
}
@@ -1021,8 +1024,6 @@ export class CurrentUserUtils {
title: "Show preview",
toolTip: "Show preview of selected document",
btnType: ButtonType.ToggleButton,
- switchToggle: true,
- width: 100,
buttonText: "Show Preview",
icon: "eye",
click: 'toggleSchemaPreview()',
@@ -1242,7 +1243,7 @@ export class CurrentUserUtils {
static setupSearchSidebar(doc: Doc) {
if (doc.mySearchPanel === undefined) {
doc.mySearchPanel = new PrefetchProxy(Docs.Create.SearchDocument({
- backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true,
+ backgroundColor: "dimgray", ignoreClick: true, _searchDoc: true,
childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "Search Panel", system: true
})) as any as Doc;
}
@@ -1307,7 +1308,6 @@ export class CurrentUserUtils {
}, { fireImmediately: true });
// Document properties on load
doc.system = true;
- doc.darkScheme = ColorScheme.Dark;
doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode;
doc.title = Doc.CurrentUserEmail;
doc._raiseWhenDragged = true;
@@ -1326,7 +1326,6 @@ export class CurrentUserUtils {
doc.fontColor = StrCast(doc.fontColor, "black");
doc.fontHighlight = StrCast(doc.fontHighlight, "");
doc.defaultAclPrivate = BoolCast(doc.defaultAclPrivate, false);
- doc.activeCollectionBackground = StrCast(doc.activeCollectionBackground, "white");
doc.activeCollectionNestedBackground = Cast(doc.activeCollectionNestedBackground, "string", null);
doc.noviceMode = BoolCast(doc.noviceMode, true);
doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); //
@@ -1334,6 +1333,7 @@ export class CurrentUserUtils {
Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]);
doc.savedFilters = new List<Doc>();
doc.filterDocCount = 0;
+ doc.freezeChildren = "remove|add";
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
this.setupDocTemplates(doc); // sets up the template menu of templates
this.setupImportSidebar(doc); // sets up the import sidebar
@@ -1363,6 +1363,13 @@ export class CurrentUserUtils {
// });
setTimeout(() => DocServer.UPDATE_SERVER_CACHE(), 2500);
doc.fieldInfos = await Docs.setupFieldInfos();
+ if (doc.activeDashboard instanceof Doc) {
+ // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss)
+ doc.activeDashboard.colorScheme = doc.activeDashboard.colorScheme === ColorScheme.Light ? undefined : doc.activeDashboard.colorScheme;
+ }
+ if (doc.activeCollectionBackground === "white") { // temporary to avoid having to rebuild the databse for old accounts that have this set by default.
+ doc.activeCollectionBackground = undefined;
+ }
return doc;
}
@@ -1532,8 +1539,7 @@ export class CurrentUserUtils {
public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, maxHeight?: number, backgroundColor?: string) {
const tbox = Docs.Create.TextDocument("", {
_xMargin: noMargins ? 0 : undefined, _yMargin: noMargins ? 0 : undefined, annotationOn, docMaxAutoHeight: maxHeight, backgroundColor: backgroundColor,
- _width: width || 200, _height: height || 100, x: x, y: y, _fitWidth: true, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize),
- _fontFamily: StrCast(Doc.UserDoc().fontFamily), title
+ _width: width || 200, _height: height || 100, x: x, y: y, _fitWidth: true, _autoHeight: true, title
});
const template = Doc.UserDoc().defaultTextLayout;
if (template instanceof Doc) {
@@ -1642,4 +1648,7 @@ Scripting.addGlobal(function makeTopLevelFolder() {
const folder = Docs.Create.TreeDocument([], { title: "Untitled folder", _stayInCollection: true, isFolder: true });
TreeView._editTitleOnLoad = { id: folder[Id], parent: undefined };
return Doc.AddDocToList(Doc.UserDoc().myFilesystem as Doc, "data", folder);
+});
+Scripting.addGlobal(function toggleComicMode() {
+ Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic";
}); \ No newline at end of file
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index dfec9823b..66b6a1e44 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -10,6 +10,7 @@ import { LightboxView } from '../views/LightboxView';
import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView';
import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
import { Scripting } from './Scripting';
+import { SelectionManager } from './SelectionManager';
export class DocumentManager {
@@ -62,6 +63,7 @@ export class DocumentManager {
const index = this.DocumentViews.indexOf(view);
index !== -1 && this.DocumentViews.splice(index, 1);
}
+ SelectionManager.DeselectView(view);
});
//gets all views
@@ -159,7 +161,8 @@ export class DocumentManager {
originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc
finished?: () => void,
originalTarget?: Doc,
- noSelect?: boolean
+ noSelect?: boolean,
+ presZoom?: number
): Promise<void> => {
originalTarget = originalTarget ?? targetDoc;
const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
@@ -192,7 +195,7 @@ export class DocumentManager {
if (focusView) {
!noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox
focusView.focus(targetDoc, {
- originalTarget, willZoom, afterFocus: (didFocus: boolean) =>
+ originalTarget, willZoom, scale: presZoom, afterFocus: (didFocus: boolean) =>
new Promise<ViewAdjustment>(res => {
focusAndFinish(didFocus);
res();
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 421e4c6bb..f5704d2bf 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -365,6 +365,21 @@ export namespace DragManager {
const dragElements = eles.map(ele => {
if (!ele.parentNode) dragDiv.appendChild(ele);
const dragElement = ele.parentNode === dragDiv ? ele : ele.cloneNode(true) as HTMLElement;
+ const children = Array.from(dragElement.children);
+ while (children.length) { // need to replace all the maker node reference ids with new unique ids. otherwise, the clone nodes will reference the original nodes which are all hidden during the drag
+ const next = children.pop();
+ next && children.push(...Array.from(next.children));
+ if (next) {
+ ["marker-start", "marker-mid", "marker-end"].forEach(field => {
+ if (next.localName.startsWith("path") && (next.attributes as any)[field]) {
+ next.setAttribute(field, (next.attributes as any)[field].value.replace("#", "#X"));
+ }
+ });
+ if (next.localName.startsWith("marker")) {
+ next.id = "X" + next.id;
+ }
+ }
+ }
const rect = ele.getBoundingClientRect();
const scaleX = rect.width / ele.offsetWidth;
const scaleY = ele.offsetHeight ? rect.height / ele.offsetHeight : scaleX;
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 4a8011e3c..a32a8eecc 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,10 +1,6 @@
import React = require("react");
-import * as beziercurve from 'bezier-curve';
-import * as fitCurve from 'fit-curve';
-import "./InteractionUtils.scss";
import { Utils } from "../../Utils";
-import { CurrentUserUtils } from "./CurrentUserUtils";
-import { InkTool } from "../../fields/InkField";
+import "./InteractionUtils.scss";
export namespace InteractionUtils {
export const MOUSETYPE = "mouse";
@@ -93,122 +89,43 @@ export namespace InteractionUtils {
return myTouches;
}
- export function CreatePoints(points: { X: number, Y: number }[], left: number, top: number,
- color: string, width: number, strokeWidth: number, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
- dash: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean, nodefs: boolean) {
- let pts: { X: number; Y: number; }[] = [];
- if (shape) { //if any of the shape are true
- pts = makePolygon(shape, points);
- }
- else if ((points.length >= 5 && points[3].X === points[4].X) || (points.length === 4)) {
- for (var i = 0; i < points.length - 3; i += 4) {
- const array = [[points[i].X, points[i].Y], [points[i + 1].X, points[i + 1].Y], [points[i + 2].X, points[i + 2].Y], [points[i + 3].X, points[i + 3].Y]];
- for (var t = 0; t < 1; t += 0.01) {
- const point = beziercurve(t, array);
- pts.push({ X: point[0], Y: point[1] });
- }
- }
- }
- else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) {
- //pointer is up (first and last points are the same)
- const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]);
- newPoints.pop();
-
- const bezierCurves = fitCurve(newPoints, parseInt(bezier));
- for (const curve of bezierCurves) {
- for (var t = 0; t < 1; t += 0.01) {
- const point = beziercurve(t, curve);
- pts.push({ X: point[0], Y: point[1] });
- }
- }
- } else {
- pts = points.slice();
- // bcz: Ugh... this is ugly, but shapes apprently have an extra point added that is = (p[0].x,p[0].y+1) as some sort of flag. need to remove it here.
- if (pts.length > 2 && pts[pts.length - 2].X === pts[0].X && pts[pts.length - 2].Y === pts[0].Y) {
- pts.pop();
- }
- }
- if (isNaN(scalex)) {
- scalex = 1;
- }
- if (isNaN(scaley)) {
- scaley = 1;
- }
- return pts;
- }
+ export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number,
+ color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
+ dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean) {
+ const pts = shape ? makePolygon(shape, points) : points;
+ if (isNaN(scalex)) scalex = 1;
+ if (isNaN(scaley)) scaley = 1;
+ const toScr = (p: { X: number, Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
+ const strpts = bezier ?
+ pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? "" : (i === 0 ? "M" + toScr(pt) : "") + "C" + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), "") :
+ pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, "");
- export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number,
- color: string, width: number, strokeWidth: number, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
- dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean) {
- let pts: { X: number; Y: number; }[] = [];
- if (shape) { //if any of the shape are true
- pts = makePolygon(shape, points);
- }
- else if ((points.length >= 5 && points[3].X === points[4].X) || (points.length === 4)) {
- for (var i = 0; i < points.length - 3; i += 4) {
- const array = [[points[i].X, points[i].Y], [points[i + 1].X, points[i + 1].Y], [points[i + 2].X, points[i + 2].Y], [points[i + 3].X, points[i + 3].Y]];
- for (var t = 0; t < 1; t += 0.01) {
- const point = beziercurve(t, array);
- pts.push({ X: point[0], Y: point[1] });
- }
- }
- }
- else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) {
- //pointer is up (first and last points are the same)
- const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]);
- newPoints.pop();
-
- const bezierCurves = fitCurve(newPoints, parseInt(bezier));
- for (const curve of bezierCurves) {
- for (var t = 0; t < 1; t += 0.01) {
- const point = beziercurve(t, curve);
- pts.push({ X: point[0], Y: point[1] });
- }
- }
- } else {
- pts = points.slice();
- // bcz: Ugh... this is ugly, but shapes apprently have an extra point added that is = (p[0].x,p[0].y+1) as some sort of flag. need to remove it here.
- if (pts.length > 2 && pts[pts.length - 2].X === pts[0].X && pts[pts.length - 2].Y === pts[0].Y) {
- pts.pop();
- }
- }
- if (isNaN(scalex)) {
- scalex = 1;
- }
- if (isNaN(scaley)) {
- scaley = 1;
- }
- const strpts = pts.reduce((acc: string, pt: { X: number, Y: number }) => acc +
- `${(pt.X - left - width / 2) * scalex + width / 2},
- ${(pt.Y - top - width / 2) * scaley + width / 2} `, "");
const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined;
const defGuid = Utils.GenerateGuid();
- const arrowDim = Math.max(0.5, 8 / Math.log(Math.max(2, strokeWidth)));
-
- const addables = pts.map((pts, i) =>
- <svg height="10" width="10">
- <circle cx={(pts.X - left - width / 2) * scalex + width / 2} cy={(pts.Y - top - width / 2) * scaley + width / 2} r={strokeWidth / 2} stroke="black" strokeWidth={1} fill="blue"
- onDoubleClick={(e) => { console.log(i); }} pointerEvents="all" cursor="all-scroll"
- />
- </svg>);
-
+ const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements;
+ const makerStrokeWidth = strokeWidth / 2;
return (<svg fill={color}> {/* setting the svg fill sets the arrowStart fill */}
{nodefs ? (null) : <defs>
- {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : <marker id={`dot${defGuid}`} orient="auto" overflow="visible">
- <circle r={1} fill="context-stroke" />
- </marker>}
- {arrowStart !== "arrow" && arrowEnd !== "arrow" ? (null) : <marker id={`arrowStart${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7">
- <polygon points={`${arrowDim} ${-Math.max(1, arrowDim / 2)}, ${arrowDim} ${Math.max(1, arrowDim / 2)}, -1 0`} />
- </marker>}
- {arrowStart !== "arrow" && arrowEnd !== "arrow" ? (null) : <marker id={`arrowEnd${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7">
- <polygon points={`${2 - arrowDim} ${-Math.max(1, arrowDim / 2)}, ${2 - arrowDim} ${Math.max(1, arrowDim / 2)}, 3 0`} />
- </marker>}
+ {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) :
+ <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
+ <circle r={strokeWidth * 1.5} fill="context-stroke" />
+ </marker>}
+ {arrowStart !== "arrow" ? (null) :
+ <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * 1.5} refY={0} markerWidth="10" markerHeight="7">
+ <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3} points={`${3 * makerStrokeWidth} ${-makerStrokeWidth * 1.5}, ${makerStrokeWidth * 2} 0, ${3 * makerStrokeWidth} ${makerStrokeWidth * 1.5}, 0 0`} />
+ </marker>}
+ {arrowEnd !== "arrow" ? (null) :
+ <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * 1.5} refY={0} markerWidth="10" markerHeight="7">
+ <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3} points={`0 ${-makerStrokeWidth * 1.5}, ${makerStrokeWidth} 0, 0 ${makerStrokeWidth * 1.5}, ${3 * makerStrokeWidth} 0`} />
+ </marker>}
</defs>}
- <polyline
- points={strpts}
+
+ <Tag
+ d={bezier ? strpts : undefined}
+ points={bezier ? undefined : strpts}
style={{
// filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
fill: fill && fill !== "transparent" ? fill : "none",
@@ -217,14 +134,12 @@ export namespace InteractionUtils {
pointerEvents: pevents as any,
stroke: color ?? "rgb(0, 0, 0)",
strokeWidth: strokeWidth,
- strokeLinejoin: color === "rgba(245, 230, 95, 0.75)" ? "miter" : "round",
- strokeLinecap: color === "rgba(245, 230, 95, 0.75)" ? "square" : "round",
+ strokeLinecap: lineCap as any,
strokeDasharray: dashArray
}}
- markerStart={`url(#${arrowStart + "Start" + defGuid})`}
- markerEnd={`url(#${arrowEnd + "End" + defGuid})`}
+ markerStart={`url(#${arrowStart === "dot" ? arrowStart + defGuid : arrowStart + "Start" + defGuid})`}
+ markerEnd={`url(#${arrowEnd === "dot" ? arrowEnd + defGuid : arrowEnd + "End" + defGuid})`}
/>
- {/* {addables} */}
</svg>);
}
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 64da68f59..62b13e2c6 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -38,15 +38,19 @@ export class LinkManager {
const addLinkToDoc = (link: Doc) => {
const a1Prom = link?.anchor1;
const a2Prom = link?.anchor2;
- Promise.all([a1Prom, a2Prom]).then(action((all) => {
+ Promise.all([a1Prom, a2Prom]).then((all) => {
const a1 = all[0];
const a2 = all[1];
- if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
- Doc.GetProto(a1)[DirectLinksSym].add(link);
- Doc.GetProto(a2)[DirectLinksSym].add(link);
- Doc.GetProto(link)[DirectLinksSym].add(link);
- }
- }));
+ const a1ProtoProm = (link?.anchor1 as Doc)?.proto;
+ const a2ProtoProm = (link?.anchor2 as Doc)?.proto;
+ Promise.all([a1ProtoProm, a2ProtoProm]).then(action((all) => {
+ if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
+ Doc.GetProto(a1)[DirectLinksSym].add(link);
+ Doc.GetProto(a2)[DirectLinksSym].add(link);
+ Doc.GetProto(link)[DirectLinksSym].add(link);
+ }
+ }));
+ });
};
const remLinkFromDoc = (link: Doc) => {
const a1 = link?.anchor1;
@@ -100,12 +104,9 @@ export class LinkManager {
public createLinkrelationshipLists = () => {
//create new lists for link relations and their associated colors if the lists don't already exist
- if (!Doc.UserDoc().linkRelationshipList && !Doc.UserDoc().linkColorList) {
- const linkRelationshipList = new List<string>();
- const linkColorList = new List<string>();
- Doc.UserDoc().linkRelationshipList = linkRelationshipList;
- Doc.UserDoc().linkColorList = linkColorList;
- }
+ !Doc.UserDoc().linkRelationshipList && (Doc.UserDoc().linkRelationshipList = new List<string>());
+ !Doc.UserDoc().linkColorList && (Doc.UserDoc().linkColorList = new List<string>());
+ !Doc.UserDoc().linkRelationshipSizes && (Doc.UserDoc().linkRelationshipSizes = new List<number>());
}
public addLink(linkDoc: Doc, checkExists = false) {
@@ -233,8 +234,10 @@ export class LinkManager {
setTimeout(LightboxView.Next);
finished?.();
} else {
- const containerDoc = Cast(target.annotationOn, Doc, null) || target;
- const targetContext = Cast(containerDoc?.context, Doc, null);
+ const containerAnnoDoc = Cast(target.annotationOn, Doc, null);
+ const containerDoc = containerAnnoDoc || target;
+ const containerDocContext = Cast(containerDoc?.context, Doc, null);
+ const targetContext = LightboxView.LightboxDoc ? containerAnnoDoc || containerDocContext : containerDocContext;
const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined;
DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "lightbox"), finished), targetNavContext, linkDoc, undefined, sourceDoc, finished);
}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index bac13373c..e507ec3bf 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -2,7 +2,6 @@ import { action, observable, ObservableMap } from "mobx";
import { computedFn } from "mobx-utils";
import { Doc, Opt } from "../../fields/Doc";
import { DocumentType } from "../documents/DocumentTypes";
-import { CollectionSchemaView } from "../views/collections/collectionSchema/CollectionSchemaView";
import { CollectionViewType } from "../views/collections/CollectionView";
import { DocumentView } from "../views/nodes/DocumentView";
@@ -13,12 +12,10 @@ export namespace SelectionManager {
@observable IsDragging: boolean = false;
SelectedViews: ObservableMap<DocumentView, Doc> = new ObservableMap();
@observable SelectedSchemaDocument: Doc | undefined;
- @observable SelectedSchemaCollection: CollectionSchemaView | undefined;
@action
- SelectSchemaView(collectionView: Opt<CollectionSchemaView>, doc: Opt<Doc>) {
+ SelectSchemaViewDoc(doc: Opt<Doc>) {
manager.SelectedSchemaDocument = doc;
- manager.SelectedSchemaCollection = collectionView;
}
@action
SelectView(docView: DocumentView, ctrlPressed: boolean): void {
@@ -33,7 +30,6 @@ export namespace SelectionManager {
} else if (!ctrlPressed && Array.from(manager.SelectedViews.entries()).length > 1) {
Array.from(manager.SelectedViews.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false));
manager.SelectedSchemaDocument = undefined;
- manager.SelectedSchemaCollection = undefined;
manager.SelectedViews.clear();
manager.SelectedViews.set(docView, docView.rootDoc);
}
@@ -47,7 +43,6 @@ export namespace SelectionManager {
}
@action
DeselectAll(): void {
- manager.SelectedSchemaCollection = undefined;
manager.SelectedSchemaDocument = undefined;
Array.from(manager.SelectedViews.keys()).map(dv => dv.props.whenChildContentsActiveChanged(false));
manager.SelectedViews.clear();
@@ -62,8 +57,8 @@ export namespace SelectionManager {
export function SelectView(docView: DocumentView, ctrlPressed: boolean): void {
manager.SelectView(docView, ctrlPressed);
}
- export function SelectSchemaView(colSchema: Opt<CollectionSchemaView>, document: Opt<Doc>): void {
- manager.SelectSchemaView(colSchema, document);
+ export function SelectSchemaViewDoc(document: Opt<Doc>): void {
+ manager.SelectSchemaViewDoc(document);
}
const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { // wrapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed
@@ -96,9 +91,6 @@ export namespace SelectionManager {
export function SelectedSchemaDoc(): Doc | undefined {
return manager.SelectedSchemaDocument;
}
- export function SelectedSchemaCollection(): CollectionSchemaView | undefined {
- return manager.SelectedSchemaCollection;
- }
export function Docs(): Doc[] {
return Array.from(manager.SelectedViews.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking);
}
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index bd91db779..6a26dfdc7 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -19,9 +19,9 @@ export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
export enum ColorScheme {
- Dark = "Dark",
- Light = "Light",
- System = "Match System"
+ Dark = "-Dark",
+ Light = "-Light",
+ System = "-MatchSystem"
}
@observer
@@ -38,7 +38,7 @@ export class SettingsManager extends React.Component<{}> {
@observable activeTab = "Accounts";
@computed get backgroundColor() { return Doc.UserDoc().activeCollectionBackground; }
- @computed get colorScheme() { return Doc.UserDoc().colorScheme; }
+ @computed get colorScheme() { return CurrentUserUtils.ActiveDashboard.colorScheme; }
constructor(props: {}) {
super(props);
@@ -81,16 +81,16 @@ export class SettingsManager extends React.Component<{}> {
const scheme: ColorScheme = (e.currentTarget as any).value;
switch (scheme) {
case ColorScheme.Light:
- Doc.UserDoc().colorScheme = ColorScheme.Light;
+ CurrentUserUtils.ActiveDashboard.colorScheme = undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss)
addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "#d3d3d3 !important" });
break;
case ColorScheme.Dark:
- Doc.UserDoc().colorScheme = ColorScheme.Dark;
+ CurrentUserUtils.ActiveDashboard.colorScheme = ColorScheme.Dark;
addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "black !important" });
break;
case ColorScheme.System: default:
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
- Doc.UserDoc().colorScheme = e.matches ? ColorScheme.Dark : ColorScheme.Light;
+ CurrentUserUtils.ActiveDashboard.colorScheme = e.matches ? ColorScheme.Dark : undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss)
});
break;
}
@@ -119,6 +119,7 @@ export class SettingsManager extends React.Component<{}> {
</div>;
const colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.System];
+ const schemeMap = ["Light", "Dark", "Match system"];
return <div className="colors-content">
<div className="preferences-color">
@@ -132,8 +133,8 @@ export class SettingsManager extends React.Component<{}> {
<div className="preferences-colorScheme">
<div className="preferences-color-text">Color Scheme</div>
<div className="preferences-color-controls">
- <select className="scheme-select" onChange={this.changeColorScheme} defaultValue={StrCast(Doc.UserDoc().colorScheme)}>
- {colorSchemes.map(scheme => <option key={scheme} value={scheme}> {scheme} </option>)}
+ <select className="scheme-select" onChange={this.changeColorScheme} defaultValue={StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme)}>
+ {colorSchemes.map((scheme, i) => <option key={scheme} value={scheme}> {schemeMap[i]} </option>)}
</select>
</div>
</div>
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index 9dc57dd1e..2de636f21 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -40,7 +40,6 @@
.permissions-select {
z-index: 1;
- margin-left: -115;
border: none;
outline: none;
text-align: justify; // for Edge
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 05fb9f378..44e44d335 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -70,6 +70,7 @@ export namespace UndoManager {
export interface UndoEvent {
undo: () => void;
redo: () => void;
+ prop: string;
}
type UndoBatch = UndoEvent[];
@@ -104,6 +105,23 @@ export namespace UndoManager {
export function GetOpenBatches(): Without<Batch, 'end'>[] {
return openBatches;
}
+ export function FilterBatches(fieldTypes: string[]) {
+ const fieldCounts: { [key: string]: number } = {};
+ const lastStack = UndoManager.undoStack.lastElement();
+ if (lastStack) {
+ lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1));
+ const fieldCount2: { [key: string]: number } = {};
+ runInAction(() =>
+ UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => {
+ if (fieldTypes.includes(ev.prop)) {
+ fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1;
+ if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true;
+ return false;
+ }
+ return true;
+ }));
+ }
+ }
export function TraceOpenBatches() {
console.log(`Open batches:\n\t${openBatches.map(batch => batch.batchName).join("\n\t")}\n`);
}
@@ -140,6 +158,7 @@ export namespace UndoManager {
batchCounter--;
// console.log("End " + batchCounter);
if (batchCounter === 0 && currentBatch?.length) {
+ // console.log("------ended----")
if (!cancel) {
undoStack.push(currentBatch);
}
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index fe6d39ca4..0f1fc6b69 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -16,7 +16,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
@observable protected _top: number = -300;
@observable protected _left: number = -300;
- @observable protected _opacity: number = 1;
+ @observable protected _opacity: number = 0;
@observable protected _transitionProperty: string = "opacity";
@observable protected _transitionDuration: string = "0.5s";
@observable protected _transitionDelay: string = "";
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 47ae0424b..ea24dbf6d 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -8,7 +8,6 @@
flex-direction: column;
background: whitesmoke;
border-radius: 3px;
- border: solid $light-gray 1px;
}
// .contextMenu-item:first-child {
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 78564a11b..80ff16cf9 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -26,8 +26,7 @@ export class ContextMenu extends React.Component {
@observable private _mouseX: number = -1;
@observable private _mouseY: number = -1;
@observable private _shouldDisplay: boolean = false;
- @observable private _mouseDown: boolean = false;
-
+ private _ignoreUp = false;
private _reactionDisposer?: IReactionDisposer;
constructor(props: Readonly<{}>) {
@@ -36,17 +35,23 @@ export class ContextMenu extends React.Component {
ContextMenu.Instance = this;
}
+ public setIgnoreEvents(ignore: boolean) {
+ this._ignoreUp = ignore;
+ }
+
@action
onPointerDown = (e: PointerEvent) => {
- this._mouseDown = true;
this._mouseX = e.clientX;
this._mouseY = e.clientY;
}
@action
onPointerUp = (e: PointerEvent) => {
- this._mouseDown = false;
const curX = e.clientX;
const curY = e.clientY;
+ if (this._ignoreUp) {
+ this._ignoreUp = false;
+ return;
+ }
if (Math.abs(this._mouseX - curX) > 1 || Math.abs(this._mouseY - curY) > 1) {
this._shouldDisplay = false;
}
@@ -62,7 +67,7 @@ export class ContextMenu extends React.Component {
componentWillUnmount() {
document.removeEventListener("pointerdown", this.onPointerDown);
document.removeEventListener("pointerup", this.onPointerUp);
- this._reactionDisposer && this._reactionDisposer();
+ this._reactionDisposer?.();
}
@action
@@ -70,10 +75,6 @@ export class ContextMenu extends React.Component {
document.addEventListener("pointerdown", this.onPointerDown);
document.addEventListener("pointerup", this.onPointerUp);
- this._reactionDisposer = reaction(
- () => this._shouldDisplay,
- () => this._shouldDisplay && !this._mouseDown && runInAction(() => this._display = true)
- );
}
@action
@@ -156,6 +157,7 @@ export class ContextMenu extends React.Component {
this._searchString = initSearch;
this._shouldDisplay = true;
this._onDisplay = onDisplay;
+ this._display = !onDisplay;
}
@action
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 7f428a881..aa9318310 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -28,6 +28,7 @@ import { PresBox } from './nodes/trails/PresBox';
import { undoBatch } from '../util/UndoManager';
import { CollectionViewType } from './collections/CollectionView';
import { Colors } from './global/globalEnums';
+import { DashFieldView } from './nodes/formattedText/DashFieldView';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -336,9 +337,9 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
openContextMenu = (e: React.MouseEvent) => {
let child = SelectionManager.Views()[0].ContentDiv!.children[0];
while (child.children.length) {
- const next = Array.from(child.children).find(c => typeof (c.className) === "string");
- if (next?.className.includes(DocumentView.ROOT_DIV)) break;
- if (next?.className.includes("dashFieldView")) break;
+ const next = Array.from(child.children).find(c => c.className?.toString().includes("SVGAnimatedString") || typeof (c.className) === "string");
+ if (next?.className?.toString().includes(DocumentView.ROOT_DIV)) break;
+ if (next?.className?.toString().includes(DashFieldView.name)) break;
if (next) child = next;
else break;
}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index a9f50f81b..d8ad47ecb 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -2,10 +2,14 @@
$linkGap: 3px;
+.documentDecorations-Dark,
.documentDecorations {
position: absolute;
z-index: 2000;
}
+.documentDecorations-Dark {
+ background: dimgray;
+}
.documentDecorations-container {
z-index: $docDecorations-zindex;
position: absolute;
@@ -50,12 +54,17 @@ $linkGap: 3px;
pointer-events: auto;
background: $medium-gray;
opacity: 0.1;
-
&:hover {
opacity: 1;
}
}
+ .documentDecorations-resizer-Dark
+ {
+ background: $light-gray;
+ opacity: 0.2;
+ }
+
.documentDecorations-topLeftResizer,
.documentDecorations-leftResizer,
.documentDecorations-bottomLeftResizer {
@@ -221,6 +230,7 @@ $linkGap: 3px;
cursor: ns-resize;
}
+ .documentDecorations-title-Dark,
.documentDecorations-title {
opacity: 1;
grid-column-start: 2;
@@ -233,14 +243,22 @@ $linkGap: 3px;
height: 22px;
position: absolute;
- .documentDecorations-titleSpan {
+ .documentDecorations-titleSpan,
+ .documentDecorations-titleSpan-Dark {
width: 100%;
border-radius: 8px;
- background: #ffffffcf;
+ background: #ffffffa0;
position: absolute;
display: inline-block;
cursor: move;
}
+ .documentDecorations-titleSpan-Dark {
+ background: hsla(0, 0%, 0%, 0.412);
+ }
+ }
+ .documentDecorations-title-Dark {
+ color: white;
+ background: black;
}
.documentDecorations-contextMenu {
@@ -439,10 +457,6 @@ $linkGap: 3px;
}
}
-.documentDecorations-darkScheme {
- background: dimgray;
-}
-
#template-list {
position: absolute;
top: 25px;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6f9697703..5b44a0552 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -8,7 +8,7 @@ import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSy
import { Document } from '../../fields/documentSchemas';
import { InkField } from "../../fields/InkField";
import { ScriptField } from '../../fields/ScriptField';
-import { Cast, NumCast } from "../../fields/Types";
+import { Cast, NumCast, StrCast } from "../../fields/Types";
import { GetEffectiveAcl } from '../../fields/util';
import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../Utils";
import { Docs } from "../documents/Documents";
@@ -27,6 +27,8 @@ import { LightboxView } from './LightboxView';
import { DocumentView } from "./nodes/DocumentView";
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import React = require("react");
+import { dark } from '@material-ui/core/styles/createPalette';
+import { color } from 'd3-color';
@observer
export class DocumentDecorations extends React.Component<{ PanelWidth: number, PanelHeight: number, boundsLeft: number, boundsTop: number }, { value: string }> {
@@ -193,14 +195,19 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
@action
onRotateDown = (e: React.PointerEvent): void => {
this._rotateUndo = UndoManager.StartBatch("rotatedown");
+ const pt = { x: (this.Bounds.x + this.Bounds.r) / 2, y: (this.Bounds.y + this.Bounds.b) / 2 };
setupMoveUpEvents(this, e,
(e: PointerEvent, down: number[], delta: number[]) => {
const movement = { X: delta[0], Y: e.clientY - down[1] };
const angle = Math.max(1, Math.abs(movement.Y / 10));
- InkStrokeProperties.Instance?.rotateInk(2 * movement.X / angle * (Math.PI / 180));
+ const selectedInk = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK);
+ InkStrokeProperties.Instance?.rotateInk(selectedInk, 2 * movement.X / angle * (Math.PI / 180), pt);
return false;
},
- () => this._rotateUndo?.end(),
+ () => {
+ this._rotateUndo?.end();
+ UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+ },
emptyFunction);
this._prevY = e.clientY;
this._inkCenterPts = SelectionManager.Views()
@@ -241,7 +248,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
InkStrokeProperties.Instance?._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK)
.forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc));
- if (fixedAspect && (this._resizeHdlId === "documentDecorations-bottomRightResizer" || this._resizeHdlId === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles
+ const resizeHdl = this._resizeHdlId.split(" ")[0];
+ if (fixedAspect && (resizeHdl === "documentDecorations-bottomRightResizer" || resizeHdl === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles
const project = (p: number[], a: number[], b: number[]) => {
const atob = [b[0] - a[0], b[1] - a[1]];
const atop = [p[0] - a[0], p[1] - a[1]];
@@ -264,7 +272,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
this._snapY = thisPt.y;
let dragBottom = false, dragRight = false, dragBotRight = false;
let dX = 0, dY = 0, dW = 0, dH = 0;
- switch (this._resizeHdlId) {
+ switch (this._resizeHdlId.split(" ")[0]) {
case "": break;
case "documentDecorations-topLeftResizer":
dX = -1;
@@ -280,6 +288,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
case "documentDecorations-topResizer":
dY = -1;
dH = -move[1];
+ dragBottom = true;
break;
case "documentDecorations-bottomLeftResizer":
dX = -1;
@@ -311,59 +320,63 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
const doc = Document(docView.rootDoc);
const nwidth = docView.nativeWidth;
const nheight = docView.nativeHeight;
- const width = (doc._width || 0);
- let height = (doc._height || (nheight / nwidth * width));
+ const docheight = doc._height || 0;
+ const docwidth = doc._width || 0;
+ const width = docwidth;
+ let height = (docheight || (nheight / nwidth * width));
height = !height || isNaN(height) ? 20 : height;
const scale = docView.props.ScreenToLocalTransform().Scale;
- const canModifyNativeDim = e.ctrlKey || doc.allowReflow;
+ const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable;
if (nwidth && nheight) {
if (nwidth / nheight !== width / height && !dragBottom) {
height = nheight / nwidth * width;
}
- if (canModifyNativeDim && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction
+ if (modifyNativeDim && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction
if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth;
else dW = dH * nwidth / nheight;
}
}
- const actualdW = Math.max(width + (dW * scale), 20);
- const actualdH = Math.max(height + (dH * scale), 20);
- doc.x = (doc.x || 0) + dX * (actualdW - width);
- doc.y = (doc.y || 0) + dY * (actualdH - height);
+ let actualdW = Math.max(width + (dW * scale), 20);
+ let actualdH = Math.max(height + (dH * scale), 20);
const fixedAspect = (nwidth && nheight);
- if (canModifyNativeDim && [DocumentType.IMG, DocumentType.SCREENSHOT, DocumentType.VID].includes(doc.type as DocumentType)) {
- dW !== 0 && runInAction(() => {
- const dataDoc = doc[DataSym];
- const nw = Doc.NativeWidth(dataDoc);
- const nh = Doc.NativeHeight(dataDoc);
- Doc.SetNativeWidth(dataDoc, nw + (dW > 0 ? 10 : -10));
- Doc.SetNativeHeight(dataDoc, nh + (dW > 0 ? 10 : -10) * nh / nw);
- });
- }
- else if (fixedAspect) {
- if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !canModifyNativeDim)) || dragRight) {
- if (dragRight && canModifyNativeDim) {
+ if (fixedAspect) {
+ if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) {
+ if (dragRight && modifyNativeDim) {
doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc);
} else {
- if (!doc._fitWidth) doc._height = nheight / nwidth * actualdW;
- else if (!canModifyNativeDim || dragBotRight) doc._height = actualdH;
+ if (!doc._fitWidth) {
+ actualdH = nheight / nwidth * actualdW;
+ doc._height = actualdH;
+ }
+ else if (!modifyNativeDim || dragBotRight) doc._height = actualdH;
}
doc._width = actualdW;
}
else {
- if (dragBottom && (canModifyNativeDim || docView.layoutDoc._fitWidth)) { // frozen web pages and others that fitWidth can't grow horizontally to match a vertical resize so the only choice is to change the nativeheight even if the ctrl key isn't used
+ if (dragBottom && (modifyNativeDim ||
+ (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) { // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used)
doc._nativeHeight = actualdH / (doc._height || 1) * Doc.NativeHeight(doc);
doc._autoHeight = false;
} else {
- if (!doc._fitWidth) doc._width = nwidth / nheight * actualdH;
- else if (!canModifyNativeDim || dragBotRight) doc._width = actualdW;
+ if (!doc._fitWidth) {
+ actualdW = nwidth / nheight * actualdH;
+ doc._width = actualdW;
+ }
+ else if (!modifyNativeDim || dragBotRight) doc._width = actualdW;
}
- doc._height = actualdH;
+ if (!modifyNativeDim) {
+ actualdH = Math.min(nheight / nwidth * NumCast(doc._width), actualdH);
+ doc._height = actualdH;
+ }
+ else doc._height = actualdH;
}
} else {
dH && (doc._height = actualdH);
dW && (doc._width = actualdW);
dH && (doc._autoHeight = false);
}
+ doc.x = (doc.x || 0) + dX * (actualdW - docwidth);
+ doc.y = (doc.y || 0) + dY * (actualdH - docheight);
doc._lastModified = new DateField();
}
const val = this._dragHeights.get(docView.layoutDoc);
@@ -420,7 +433,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
return (null);
}
- const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection && !docView.props.Document.isGroup);
+ const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles;
+ const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle;
+ const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection && !docView.props.Document.isGroup && !docView.props.Document.hideOpenButton);
const canDelete = SelectionManager.Views().some(docView => {
const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit;
return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) &&
@@ -434,11 +449,17 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
</div>
</Tooltip>);
+ const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme);
const titleArea = this._edtingTitle ?
- <input ref={this._keyinput} className="documentDecorations-title" style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }} type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle}
- onBlur={e => this.titleBlur()} onChange={action(e => this._accumulatedTitle = e.target.value)} onKeyPress={this.titleEntered} /> :
+ <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`}
+ style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }}
+ type="text" name="dynbox" autoComplete="on"
+ value={this._accumulatedTitle}
+ onBlur={e => this.titleBlur()}
+ onChange={action(e => this._accumulatedTitle = e.target.value)}
+ onKeyPress={this.titleEntered} /> :
<div className="documentDecorations-title" style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} >
- <span className="documentDecorations-titleSpan">{`${this.selectionTitle}`}</span>
+ <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
</div>;
let inMainMenuPanel = false;
@@ -454,8 +475,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
const useRotation = seldoc.rootDoc.type === DocumentType.INK;
+ const resizerScheme = colorScheme ? "documentDecorations-resizer" + colorScheme : "";
- return (<div className="documentDecorations" style={{ background: CurrentUserUtils.ActiveDashboard?.darkScheme ? "dimgray" : "" }} >
+ return (<div className={`documentDecorations${colorScheme}`}>
<div className="documentDecorations-background" style={{
width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px",
@@ -472,21 +494,21 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
}}>
{!canDelete ? <div /> : topBtn("close", "times", undefined, this.onCloseClick, "Close")}
- {seldoc.props.hideDecorationTitle || seldoc.props.Document.type === DocumentType.EQUATION ? (null) : titleArea}
- {seldoc.props.hideResizeHandles || seldoc.props.Document.type === DocumentType.EQUATION ? (null) :
+ {hideTitle ? (null) : titleArea}{!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
+
+ {hideResizers ? (null) :
<>
- {SelectionManager.Views().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) :
+ {SelectionManager.Views().length !== 1 || hideTitle ? (null) :
topBtn("iconify", `window-${seldoc.finalLayoutKey.includes("icon") ? "restore" : "minimize"}`, undefined, this.onIconifyClick, `${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`)}
- {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
- <div key="tl" className="documentDecorations-topLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="t" className="documentDecorations-topResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="tr" className="documentDecorations-topRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="l" className="documentDecorations-leftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="c" className="documentDecorations-centerCont"></div>
- <div key="r" className="documentDecorations-rightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="bl" className="documentDecorations-bottomLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="b" className="documentDecorations-bottomResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="br" className="documentDecorations-bottomRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="t" className={`documentDecorations-topResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="tr" className={`documentDecorations-topRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="l" className={`documentDecorations-leftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="c" className={`documentDecorations-centerCont ${resizerScheme}`}></div>
+ <div key="r" className={`documentDecorations-rightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="bl" className={`documentDecorations-bottomLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="b" className={`documentDecorations-bottomResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="br" className={`documentDecorations-bottomRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
{seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) :
topBtn("selector", "arrow-alt-circle-up", undefined, this.onSelectorClick, "tap to select containing document")}
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 6ccbd3fd7..f28485e43 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -627,6 +627,9 @@ export class GestureOverlay extends Touchable {
}
+ const dist = Math.sqrt((controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) +
+ (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y));
+ if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0];
this._points = controlPoints;
this.dispatchGesture(GestureUtils.Gestures.Stroke);
@@ -708,7 +711,7 @@ export class GestureOverlay extends Touchable {
this._points.push({ X: left, Y: top });
break;
-
+
case "triangle":
this._points.push({ X: left, Y: bottom });
this._points.push({ X: left, Y: bottom });
@@ -761,10 +764,10 @@ export class GestureOverlay extends Touchable {
break;
case "line":
- if (Math.abs(firstx - lastx) < 20) {
+ if (Math.abs(firstx - lastx) < 10 && Math.abs(firsty - lasty) > 10) {
lastx = firstx;
}
- if (Math.abs(firsty - lasty) < 20) {
+ if (Math.abs(firsty - lasty) < 10 && Math.abs(firstx - lastx) > 10) {
lasty = firsty;
}
this._points.push({ X: firstx, Y: firsty });
@@ -838,20 +841,23 @@ export class GestureOverlay extends Touchable {
B.bottom = B.bottom + width / 2;
B.width += width;
B.height += width;
+ const fillColor = ActiveFillColor();
+ const strokeColor = fillColor && fillColor !== "transparent" ? fillColor : ActiveInkColor();
return [
this.props.children,
this._palette,
[this._strokes.map((l, i) => {
const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 };//this.getBounds(l, true);
return <svg key={i} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
- {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), width, width,
- ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(),
+ {InteractionUtils.CreatePolyline(l, b.left, b.top, strokeColor, width, width, "miter", "round",
+ ActiveInkBezierApprox(), "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(),
ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)}
</svg>;
}),
this._points.length <= 1 ? (null) : <svg key="svg" width={B.width} height={B.height}
style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
- {InteractionUtils.CreatePolyline(this._points.map(p => ({ X: p.X, Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)}
+ {InteractionUtils.CreatePolyline(this._points.map(p => ({ X: p.X, Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, "miter", "round", "",
+ "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)}
</svg>]
];
}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index f66c9c788..364bf05e2 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -10,10 +10,11 @@ import { Cast, PromiseValue } from "../../fields/Types";
import { GoogleAuthenticationManager } from "../apis/GoogleAuthenticationManager";
import { DocServer } from "../DocServer";
import { DocumentType } from "../documents/DocumentTypes";
-import { DictationManager } from "../util/DictationManager";
+import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { DragManager } from "../util/DragManager";
import { GroupManager } from "../util/GroupManager";
import { SelectionManager } from "../util/SelectionManager";
+import { SettingsManager } from "../util/SettingsManager";
import { SharingManager } from "../util/SharingManager";
import { SnappingManager } from "../util/SnappingManager";
import { undoBatch, UndoManager } from "../util/UndoManager";
@@ -27,8 +28,6 @@ import { LightboxView } from "./LightboxView";
import { MainView } from "./MainView";
import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
import { AnchorMenu } from "./pdf/AnchorMenu";
-import { CurrentUserUtils } from "../util/CurrentUserUtils";
-import { SettingsManager } from "../util/SettingsManager";
const modifiers = ["control", "meta", "shift", "alt"];
type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -142,7 +141,7 @@ export class KeyManager {
case "delete":
case "backspace":
if (document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA") {
- const selected = SelectionManager.Views().slice();
+ const selected = SelectionManager.Views().filter(dv => !dv.topMost);
UndoManager.RunInBatch(() => {
SelectionManager.DeselectAll();
selected.map(dv => !dv.props.Document._stayInCollection && dv.props.removeDocument?.(dv.props.Document));
@@ -222,10 +221,13 @@ export class KeyManager {
PromiseValue(Cast(Doc.UserDoc()["tabs-button-tools"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
break;
case "f":
- const searchBtn = Doc.UserDoc().searchBtn as Doc;
-
- if (searchBtn) {
- MainView.Instance.selectMenu(searchBtn);
+ if (SelectionManager.Views().length === 1 && SelectionManager.Views()[0].ComponentView?.search) {
+ SelectionManager.Views()[0].ComponentView?.search?.("", false, false);
+ } else {
+ const searchBtn = Doc.UserDoc().searchBtn as Doc;
+ if (searchBtn) {
+ MainView.Instance.selectMenu(searchBtn);
+ }
}
break;
case "o":
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
new file mode 100644
index 000000000..73de4a3e0
--- /dev/null
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -0,0 +1,176 @@
+import React = require("react");
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Doc } from "../../fields/Doc";
+import { ControlPoint, InkData, PointData } from "../../fields/InkField";
+import { List } from "../../fields/List";
+import { listSpec } from "../../fields/Schema";
+import { Cast } from "../../fields/Types";
+import { setupMoveUpEvents } from "../../Utils";
+import { Transform } from "../util/Transform";
+import { UndoManager } from "../util/UndoManager";
+import { Colors } from "./global/globalEnums";
+import { InkingStroke } from "./InkingStroke";
+import { InkStrokeProperties } from "./InkStrokeProperties";
+import { DocumentView } from "./nodes/DocumentView";
+
+export interface InkControlProps {
+ inkDoc: Doc;
+ inkView: DocumentView;
+ inkCtrlPoints: InkData;
+ screenCtrlPoints: InkData;
+ screenSpaceLineWidth: number;
+ ScreenToLocalTransform: () => Transform;
+ nearestScreenPt: () => PointData | undefined;
+}
+
+@observer
+export class InkControlPtHandles extends React.Component<InkControlProps> {
+
+ @observable private _overControl = -1;
+
+ @observable controlUndo: UndoManager.Batch | undefined;
+
+ componentDidMount() {
+ document.addEventListener("keydown", this.onDelete, true);
+ }
+ componentWillUnmount() {
+ document.removeEventListener("keydown", this.onDelete, true);
+ }
+ /**
+ * Handles the movement of a selected control point when the user clicks and drags.
+ * @param controlIndex The index of the currently selected control point.
+ */
+ @action
+ onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
+ if (InkStrokeProperties.Instance) {
+ const screenScale = this.props.ScreenToLocalTransform().Scale;
+ const order = controlIndex % 4;
+ const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
+ const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length;
+ const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
+ const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex;
+ setupMoveUpEvents(this, e,
+ action((e: PointerEvent, down: number[], delta: number[]) => {
+ if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
+ InkStrokeProperties.Instance?.moveControlPtHandle(this.props.inkView, delta[0] * screenScale, delta[1] * screenScale, controlIndex);
+ return false;
+ }),
+ action(() => {
+ if (this.controlUndo) {
+ InkStrokeProperties.Instance?.snapControl(this.props.inkView, controlIndex);
+ }
+ this.controlUndo?.end();
+ this.controlUndo = undefined;
+ UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+ }),
+ action((e: PointerEvent, doubleTap: boolean | undefined) => {
+ const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
+ if (doubleTap || e.button === 2) {
+ if (!brokenIndices?.includes(equivIndex) && !brokenIndices?.includes(controlIndex)) {
+ if (brokenIndices) brokenIndices.push(controlIndex);
+ else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
+ } else {
+ if (brokenIndices?.includes(equivIndex)) {
+ if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
+ InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB);
+ }
+ if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
+ if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
+ InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB);
+ }
+ }
+ this.controlUndo?.end();
+ this.controlUndo = undefined;
+ }
+ this.changeCurrPoint(controlIndex);
+ }), undefined, undefined, () => wasSelected && this.changeCurrPoint(-1));
+ }
+ }
+ /**
+ * Updates whether a user has hovered over a particular control point or point that could be added
+ * on click.
+ */
+ @action onEnterControl = (i: number) => { this._overControl = i; };
+ @action onLeaveControl = () => { this._overControl = -1; };
+
+ /**
+ * Deletes the currently selected point.
+ */
+ @action
+ onDelete = (e: KeyboardEvent) => {
+ if (["-", "Backspace", "Delete"].includes(e.key)) {
+ InkStrokeProperties.Instance?.deletePoints(this.props.inkView);
+ e.stopPropagation();
+ }
+ }
+
+ /**
+ * Changes the current selected control point.
+ */
+ @action
+ changeCurrPoint = (i: number) => {
+ if (InkStrokeProperties.Instance) {
+ InkStrokeProperties.Instance._currentPoint = i;
+ }
+ }
+
+ render() {
+ // Accessing the current ink's data and extracting all control points.
+ const scrData = this.props.screenCtrlPoints;
+ const sreenCtrlPoints: ControlPoint[] = [];
+ for (let i = 0; i <= scrData.length - 4; i += 4) {
+ sreenCtrlPoints.push({ ...scrData[i], I: i });
+ sreenCtrlPoints.push({ ...scrData[i + 3], I: i + 3 });
+ }
+
+ const inkData = this.props.inkCtrlPoints;
+ const inkCtrlPts: ControlPoint[] = [];
+ for (let i = 0; i <= inkData.length - 4; i += 4) {
+ inkCtrlPts.push({ ...inkData[i], I: i });
+ inkCtrlPts.push({ ...inkData[i + 3], I: i + 3 });
+ }
+
+ const screenSpaceLineWidth = this.props.screenSpaceLineWidth;
+ const closed = InkingStroke.IsClosed(inkData);
+ const nearestScreenPt = this.props.nearestScreenPt();
+ const TagType = (broken?: boolean) => broken ? "rect" : "circle";
+ const hdl = (control: { X: number, Y: number, I: number }, scale: number, color: string) => {
+ const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"))?.includes(control.I);
+ const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements;
+ return <Tag key={control.I.toString() + scale}
+ x={control.X - screenSpaceLineWidth * 2 * scale}
+ y={control.Y - screenSpaceLineWidth * 2 * scale}
+ cx={control.X}
+ cy={control.Y}
+ r={screenSpaceLineWidth * 2 * scale}
+ opacity={this.controlUndo ? 0.15 : 1}
+ height={screenSpaceLineWidth * 4 * scale}
+ width={screenSpaceLineWidth * 4 * scale}
+ strokeWidth={screenSpaceLineWidth / 2}
+ stroke={Colors.MEDIUM_BLUE}
+ fill={broken ? Colors.MEDIUM_BLUE : color}
+ onPointerDown={(e: any) => this.onControlDown(e, control.I)}
+ onMouseEnter={() => this.onEnterControl(control.I)}
+ onMouseLeave={this.onLeaveControl}
+ pointerEvents="all"
+ cursor="default"
+ />;
+ };
+ return (<svg>
+ {!nearestScreenPt ? (null) :
+ <circle key={"npt"}
+ cx={nearestScreenPt.X}
+ cy={nearestScreenPt.Y}
+ r={screenSpaceLineWidth * 2}
+ fill={"#00007777"}
+ stroke={"#00007777"}
+ strokeWidth={0}
+ pointerEvents="none"
+ />
+ }
+ {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))}
+ </svg>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx
deleted file mode 100644
index 4df7ee813..000000000
--- a/src/client/views/InkControls.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import React = require("react");
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import { Doc } from "../../fields/Doc";
-import { ControlPoint, InkData, PointData } from "../../fields/InkField";
-import { listSpec } from "../../fields/Schema";
-import { Cast } from "../../fields/Types";
-import { setupMoveUpEvents } from "../../Utils";
-import { Transform } from "../util/Transform";
-import { UndoManager } from "../util/UndoManager";
-import { Colors } from "./global/globalEnums";
-import { InkStrokeProperties } from "./InkStrokeProperties";
-
-export interface InkControlProps {
- inkDoc: Doc;
- inkCtrlPoints: InkData;
- screenCtrlPoints: InkData;
- inkStrokeSamplePts: PointData[];
- screenStrokeSamplePoints: PointData[];
- format: number[];
- ScreenToLocalTransform: () => Transform;
-}
-
-@observer
-export class InkControls extends React.Component<InkControlProps> {
- @observable private _overControl = -1;
- @observable private _overAddPoint = -1;
-
- /**
- * Handles the movement of a selected control point when the user clicks and drags.
- * @param controlIndex The index of the currently selected control point.
- */
- @action
- onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
- if (InkStrokeProperties.Instance) {
- InkStrokeProperties.Instance.moveControl(0, 0, 1);
- const controlUndo = UndoManager.StartBatch("DocDecs set radius");
- const screenScale = this.props.ScreenToLocalTransform().Scale;
- const order = controlIndex % 4;
- const handleIndexA = order === 2 ? controlIndex - 1 : controlIndex - 2;
- const handleIndexB = order === 2 ? controlIndex + 2 : controlIndex + 1;
- const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
- setupMoveUpEvents(this, e,
- (e: PointerEvent, down: number[], delta: number[]) => {
- InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex);
- return false;
- },
- () => controlUndo?.end(),
- action((e: PointerEvent, doubleTap: boolean | undefined) => {
- if (doubleTap && brokenIndices && brokenIndices.includes(controlIndex)) {
- InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
- }
- }));
- }
- }
-
- /**
- * Deletes the currently selected point.
- */
- @action
- onDelete = (e: KeyboardEvent) => {
- if (["-", "Backspace", "Delete"].includes(e.key)) {
- if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation();
- }
- }
-
- /**
- * Changes the current selected control point.
- */
- @action
- changeCurrPoint = (i: number) => {
- if (InkStrokeProperties.Instance) {
- InkStrokeProperties.Instance._currentPoint = i;
- document.addEventListener("keydown", this.onDelete, true);
- }
- }
-
- /**
- * Updates whether a user has hovered over a particular control point or point that could be added
- * on click.
- */
- @action onEnterControl = (i: number) => { this._overControl = i; };
- @action onLeaveControl = () => { this._overControl = -1; };
- @action onEnterAddPoint = (i: number) => { this._overAddPoint = i; };
- @action onLeaveAddPoint = () => { this._overAddPoint = -1; };
-
- render() {
- const formatInstance = InkStrokeProperties.Instance;
- if (!formatInstance) return (null);
-
- // Accessing the current ink's data and extracting all control points.
- const scrData = this.props.screenCtrlPoints;
- const sreenCtrlPoints: ControlPoint[] = [];
- for (let i = 0; i <= scrData.length - 4; i += 4) {
- sreenCtrlPoints.push({ X: scrData[i].X, Y: scrData[i].Y, I: i });
- sreenCtrlPoints.push({ X: scrData[i + 3].X, Y: scrData[i + 3].Y, I: i + 3 });
- }
-
- const inkData = this.props.inkCtrlPoints;
- const inkCtrlPts: ControlPoint[] = [];
- for (let i = 0; i <= inkData.length - 4; i += 4) {
- inkCtrlPts.push({ X: inkData[i].X, Y: inkData[i].Y, I: i });
- inkCtrlPts.push({ X: inkData[i + 3].X, Y: inkData[i + 3].Y, I: i + 3 });
- }
-
- const [left, top, scaleX, scaleY, strokeWidth, screenSpaceLineWidth] = this.props.format;
- const rectHdlSize = (i: number) => this._overControl === i ? screenSpaceLineWidth * 6 : screenSpaceLineWidth * 4;
- return (<svg>
- {/* should really have just one circle here that represents the neqraest point on the stroke to the users hover point.
- This points should be passed as a prop from InkingStroke's UI which should set it in its onPointerOver method */}
- {this.props.screenStrokeSamplePoints.map((pts, i) =>
- <circle key={i}
- cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2}
- cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
- r={screenSpaceLineWidth * 4}
- fill={this._overAddPoint === i ? "#00007777" : "transparent"}
- stroke={this._overAddPoint === i ? "#00007777" : "transparent"}
- strokeWidth={0}
- onPointerDown={() => formatInstance?.addPoints(this.props.inkStrokeSamplePts[i].X, this.props.inkStrokeSamplePts[i].Y, this.props.inkStrokeSamplePts, i, inkCtrlPts)}
- onMouseEnter={() => this.onEnterAddPoint(i)}
- onMouseLeave={this.onLeaveAddPoint}
- pointerEvents="all"
- cursor="all-scroll"
- />
- )}
- {sreenCtrlPoints.map((control, i) =>
- <rect key={i}
- x={(control.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2 - rectHdlSize(i) / 2}
- y={(control.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2 - rectHdlSize(i) / 2}
- height={rectHdlSize(i)}
- width={rectHdlSize(i)}
- strokeWidth={screenSpaceLineWidth / 2}
- stroke={Colors.MEDIUM_BLUE}
- fill={formatInstance?._currentPoint === control.I ? Colors.MEDIUM_BLUE : Colors.WHITE}
- onPointerDown={(e) => {
- this.changeCurrPoint(control.I);
- this.onControlDown(e, control.I);
- }}
- onMouseEnter={() => this.onEnterControl(i)}
- onMouseLeave={this.onLeaveControl}
- pointerEvents="all"
- cursor="default"
- />
- )}
- </svg>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx
deleted file mode 100644
index afe94cdfb..000000000
--- a/src/client/views/InkHandles.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import React = require("react");
-import { action } from "mobx";
-import { observer } from "mobx-react";
-import { Doc } from "../../fields/Doc";
-import { HandleLine, HandlePoint, InkData } from "../../fields/InkField";
-import { List } from "../../fields/List";
-import { listSpec } from "../../fields/Schema";
-import { Cast } from "../../fields/Types";
-import { emptyFunction, setupMoveUpEvents } from "../../Utils";
-import { Transform } from "../util/Transform";
-import { UndoManager } from "../util/UndoManager";
-import { Colors } from "./global/globalEnums";
-import { InkStrokeProperties } from "./InkStrokeProperties";
-
-export interface InkHandlesProps {
- inkDoc: Doc;
- data: InkData;
- format: number[];
- ScreenToLocalTransform: () => Transform;
-}
-
-@observer
-export class InkHandles extends React.Component<InkHandlesProps> {
- /**
- * Handles the movement of a selected handle point when the user clicks and drags.
- * @param handleNum The index of the currently selected handle point.
- */
- onHandleDown = (e: React.PointerEvent, handleIndex: number): void => {
- if (InkStrokeProperties.Instance) {
- InkStrokeProperties.Instance.moveControl(0, 0, 1);
- const controlUndo = UndoManager.StartBatch("DocDecs set radius");
- const screenScale = this.props.ScreenToLocalTransform().Scale;
- const order = handleIndex % 4;
- const oppositeHandleIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
- const controlIndex = order === 1 ? handleIndex - 1 : handleIndex + 2;
- document.addEventListener("keydown", (e: KeyboardEvent) => this.onBreakTangent(e, controlIndex), true);
- setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
- InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
- return false;
- }, () => controlUndo?.end(), emptyFunction
- );
- }
- }
-
- /**
- * Breaks tangent handle movement when ‘Alt’ key is held down. Adds the current handle index and
- * its matching (opposite) handle to a list of broken handle indices.
- * @param handleNum The index of the currently selected handle point.
- */
- @action
- onBreakTangent = (e: KeyboardEvent, controlIndex: number) => {
- const doc: Doc = this.props.inkDoc;
- if (["Alt"].includes(e.key)) {
- e.stopPropagation();
- if (doc) {
- const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")) || new List;
- if (brokenIndices && !brokenIndices.includes(controlIndex)) {
- brokenIndices.push(controlIndex);
- }
- doc.brokenInkIndices = brokenIndices;
- }
- }
- }
-
- render() {
- const formatInstance = InkStrokeProperties.Instance;
- if (!formatInstance) return (null);
-
- // Accessing the current ink's data and extracting all handle points and handle lines.
- const data = this.props.data;
- const handlePoints: HandlePoint[] = [];
- const handleLines: HandleLine[] = [];
- if (data.length >= 4) {
- for (let i = 0; i <= data.length - 4; i += 4) {
- handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 });
- handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 });
- }
- // Adding first and last (single) handle lines.
- handleLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 });
- handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 });
- for (let i = 2; i < data.length - 4; i += 4) {
- handleLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 });
- }
- }
- const [left, top, scaleX, scaleY, strokeWidth, screenSpaceLineWidth] = this.props.format;
-
- return (
- <>
- {handlePoints.map((pts, i) =>
- <svg height="10" width="10" key={`hdl${i}`}>
- <circle
- cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2}
- cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
- r={screenSpaceLineWidth * 2}
- strokeWidth={0}
- fill={Colors.MEDIUM_BLUE}
- onPointerDown={(e) => this.onHandleDown(e, pts.I)}
- pointerEvents="all"
- cursor="default"
- display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />
- </svg>)}
- {handleLines.map((pts, i) =>
- <svg height="100" width="100" key={`line${i}`}>
- <line
- x1={(pts.X1 - left - strokeWidth / 2) * scaleX + strokeWidth / 2}
- y1={(pts.Y1 - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
- x2={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2}
- y2={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
- stroke={Colors.MEDIUM_BLUE}
- strokeWidth={screenSpaceLineWidth}
- display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />
- <line
- x1={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2}
- y1={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
- x2={(pts.X3 - left - strokeWidth / 2) * scaleX + strokeWidth / 2}
- y2={(pts.Y3 - top - strokeWidth / 2) * scaleY + strokeWidth / 2}
- stroke={Colors.MEDIUM_BLUE}
- strokeWidth={screenSpaceLineWidth}
- display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />
- </svg>)}
- </>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss
index 53d27cd24..55e06c6ca 100644
--- a/src/client/views/InkStroke.scss
+++ b/src/client/views/InkStroke.scss
@@ -3,6 +3,7 @@
position: absolute;
overflow: visible;
pointer-events: none;
+ z-index: 2001; // 1 higher than documentdecorations
svg:not(:root) {
overflow: visible !important;
@@ -18,6 +19,8 @@
stroke-linecap: round;
overflow: visible !important;
transform-origin: top left;
+ width: 100%;
+ height: 100%;
svg:not(:root) {
overflow: visible !important;
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 42190238e..33e25bbbb 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,14 +1,15 @@
-import { action, computed, observable, reaction } from "mobx";
-import { Doc, DocListCast, Field, Opt } from "../../fields/Doc";
-import { Document } from "../../fields/documentSchemas";
-import { InkField, InkData, PointData, ControlPoint, InkTool } from "../../fields/InkField";
+import { Bezier } from "bezier-js";
+import { action, observable, reaction } from "mobx";
+import { Doc, Opt } from "../../fields/Doc";
+import { InkData, InkField, InkTool, PointData } from "../../fields/InkField";
import { List } from "../../fields/List";
import { listSpec } from "../../fields/Schema";
import { Cast, NumCast } from "../../fields/Types";
import { DocumentType } from "../documents/DocumentTypes";
-import { SelectionManager } from "../util/SelectionManager";
-import { undoBatch } from "../util/UndoManager";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
+import { undoBatch } from "../util/UndoManager";
+import { InkingStroke } from "./InkingStroke";
+import { DocumentView } from "./nodes/DocumentView";
export class InkStrokeProperties {
static Instance: InkStrokeProperties | undefined;
@@ -23,39 +24,29 @@ export class InkStrokeProperties {
reaction(() => CurrentUserUtils.SelectedTool, tool => (tool !== InkTool.None) && (this._controlButton = false));
}
- @computed get selectedInk() {
- const inks = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK);
- return inks.length ? inks : undefined;
- }
-
- getField(key: string) {
- return this.selectedInk?.reduce((p, i) =>
- (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt<string>);
- }
-
/**
* Helper function that enables other functions to be applied to a particular ink instance.
* @param func The inputted function.
* @param requireCurrPoint Indicates whether the current selected point is needed.
*/
- applyFunction = (func: (doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => {
+ applyFunction = (strokes: Opt<DocumentView | DocumentView[]>, func: (view: DocumentView, ink: InkData, ptsXscale: number, ptsYscale: number, inkStrokeWidth: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => {
var appliedFunc = false;
- this.selectedInk?.forEach(action(inkView => {
- if (this.selectedInk?.length === 1 && (!requireCurrPoint || this._currentPoint !== -1)) {
- const doc = Document(inkView.rootDoc);
+ (strokes instanceof DocumentView ? [strokes] : strokes)?.forEach(action(inkView => {
+ if (!requireCurrPoint || this._currentPoint !== -1) {
+ const doc = inkView.rootDoc;
if (doc.type === DocumentType.INK && doc.width && doc.height) {
const ink = Cast(doc.data, InkField)?.inkData;
if (ink) {
const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X));
const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y));
- const ptsXscale = NumCast(doc._width) / (oldXrange.max - oldXrange.min);
- const ptsYscale = NumCast(doc._height) / (oldYrange.max - oldYrange.min);
- const newPoints = func(doc, ink, ptsXscale, ptsYscale);
+ const ptsXscale = ((NumCast(doc._width) - NumCast(doc.strokeWidth)) / ((oldXrange.max - oldXrange.min) || 1)) || 1;
+ const ptsYscale = ((NumCast(doc._height) - NumCast(doc.strokeWidth)) / ((oldYrange.max - oldYrange.min) || 1)) || 1;
+ const newPoints = func(inkView, ink, ptsXscale, ptsYscale, NumCast(doc.strokeWidth));
if (newPoints) {
const newXrange = (xs => ({ min: Math.min(...xs), max: Math.max(...xs) }))(newPoints.map(p => p.X));
const newYrange = (ys => ({ min: Math.min(...ys), max: Math.max(...ys) }))(newPoints.map(p => p.Y));
- doc._width = (newXrange.max - newXrange.min) * ptsXscale;
- doc._height = (newYrange.max - newYrange.min) * ptsYscale;
+ doc._width = (newXrange.max - newXrange.min) * ptsXscale + NumCast(doc.strokeWidth);
+ doc._height = (newYrange.max - newYrange.min) * ptsYscale + NumCast(doc.strokeWidth);
doc.x = (oldXrange.coord + (newXrange.min - oldXrange.min) * ptsXscale);
doc.y = (oldYrange.coord + (newYrange.min - oldYrange.min) * ptsYscale);
Doc.GetProto(doc).data = new InkField(newPoints);
@@ -70,66 +61,25 @@ export class InkStrokeProperties {
/**
* Adds a new control point to the ink instance when editing its format.
- * @param index The index of the new point.
+ * @param t T-Value of new control point
+ * @param i index of first control point of segment being split
* @param control The list of all control points of the ink.
*/
@undoBatch
@action
- addPoints = (x: number, y: number, points: InkData, index: number, controls: { X: number, Y: number }[]) => {
- this.applyFunction((doc: Doc, ink: InkData) => {
- const newControl = { X: x, Y: y };
- const newPoints: InkData = [];
- let [counter, start, end] = [0, 0, 0];
- for (let k = 0; k < points.length; k++) {
- if (end === 0) {
- controls.forEach((control) => {
- if (control.X === points[k].X && control.Y === points[k].Y) {
- if (k < index) {
- counter++;
- start = k;
- } else if (k > index) {
- end = k;
- }
- }
- });
- }
- }
- if (end === 0) end = points.length - 1;
- // Index of new control point with regards to the ink data.
- const newIndex = Math.floor(counter / 2) * 4 + 2;
- // Creating new ink data with the new control point and handle points inputted.
- for (let i = 0; i < ink.length; i++) {
- if (i === newIndex) {
- const [handleA, handleB] = this.getNewHandlePoints(points.slice(start, index + 1), points.slice(index, end), newControl);
- newPoints.push(handleA, newControl, newControl, handleB);
- // Adjusting the magnitude of the left handle line of the right neighboring control point.
- const [rightControl, rightHandle] = [points[end], ink[i]];
- const scaledVector = this.getScaledHandlePoint(false, start, end, index, rightControl, rightHandle);
- rightHandle && newPoints.push({ X: rightControl.X - scaledVector.X, Y: rightControl.Y - scaledVector.Y });
- } else if (i === newIndex - 1) {
- // Adjusting the magnitude of the right handle line of the left neighboring control point.
- const [leftControl, leftHandle] = [points[start], ink[i]];
- const scaledVector = this.getScaledHandlePoint(true, start, end, index, leftControl, leftHandle);
- leftHandle && newPoints.push({ X: leftControl.X - scaledVector.X, Y: leftControl.Y - scaledVector.Y });
- } else {
- ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y });
- }
+ addPoints = (inkView: DocumentView, t: number, i: number, controls: { X: number, Y: number }[]) => {
+ this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+ const doc = view.rootDoc;
+ const array = [controls[i], controls[i + 1], controls[i + 2], controls[i + 3]];
+ const newsegs = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).split(t);
+ const splicepts = [...newsegs.left.points, ...newsegs.right.points];
+ controls.splice(i, 4, ...splicepts.map(p => ({ X: p.x, Y: p.y })));
- }
- let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
// Updating the indices of the control points whose handle tangency has been broken.
- if (brokenIndices) {
- brokenIndices = new List(brokenIndices.map((control) => {
- if (control >= newIndex) {
- return control + 4;
- } else {
- return control;
- }
- }));
- }
- doc.brokenInkIndices = brokenIndices;
+ doc.brokenInkIndices = new List(Cast(doc.brokenInkIndices, listSpec("number"), []).map(control => control > i ? control + 4 : control));
this._currentPoint = -1;
- return newPoints;
+
+ return controls;
});
}
@@ -143,8 +93,7 @@ export class InkStrokeProperties {
const prevSize = end - start;
const newSize = isLeft ? index - start : end - index;
const handleVector = { X: control.X - handle.X, Y: control.Y - handle.Y };
- const scaledVector = { X: handleVector.X * (newSize / prevSize), Y: handleVector.Y * (newSize / prevSize) };
- return scaledVector;
+ return { X: handleVector.X * (newSize / prevSize), Y: handleVector.Y * (newSize / prevSize) };
}
/**
@@ -189,25 +138,20 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- deletePoints = () => this.applyFunction((doc: Doc, ink: InkData) => {
+ deletePoints = (inkView: DocumentView) => this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+ const doc = view.rootDoc;
const newPoints: { X: number, Y: number }[] = [];
- const toRemove = Math.floor(((this._currentPoint + 2) / 4));
+ const toRemove = Math.floor((this._currentPoint + 2) / 4);
+ const last = this._currentPoint === ink.length - 1;
for (let i = 0; i < ink.length; i++) {
if (Math.floor((i + 2) / 4) !== toRemove && (toRemove !== 0 || i > 3)) {
newPoints.push({ X: ink[i].X, Y: ink[i].Y });
}
}
+ doc.brokenInkIndices = new List(Cast(doc.brokenInkIndices, listSpec("number"), []).map(control => control >= toRemove * 4 ? control - 4 : control));
+ if (last) newPoints.splice(newPoints.length - 3, 2);
this._currentPoint = -1;
- if (newPoints.length < 4) return undefined;
- if (newPoints.length === 4) {
- const newerPoints: { X: number, Y: number }[] = [];
- newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y });
- newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y });
- newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y });
- newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y });
- return newerPoints;
- }
- return newPoints;
+ return newPoints.length < 4 ? undefined : newPoints;
}, true)
/**
@@ -216,17 +160,22 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- rotateInk = (angle: number) => {
- this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
- const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X));
- const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y));
- const centerPoint = { X: (oldXrange.min + oldXrange.max) / 2, Y: (oldYrange.min + oldYrange.max) / 2 };
- const newPoints: { X: number, Y: number }[] = [];
- ink.map(i => ({ X: i.X - centerPoint.X, Y: i.Y - centerPoint.Y })).forEach(i => {
- const newX = Math.cos(angle) * i.X - Math.sin(angle) * i.Y;
- const newY = Math.sin(angle) * i.X + Math.cos(angle) * i.Y;
- newPoints.push({ X: newX + centerPoint.X, Y: newY + centerPoint.Y });
+ rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: { x: number, y: number }) => {
+ this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => {
+ const oldXrangeMin = Math.min(...ink.map(p => p.X));
+ const oldYrangeMin = Math.min(...ink.map(p => p.Y));
+ const docViewCenterPt = view.screenToLocalTransform().transformPoint(scrpt.x, scrpt.y);
+ const inkCenterPt = {
+ X: (docViewCenterPt[0] - inkStrokeWidth / 2) / xScale + oldXrangeMin,
+ Y: (docViewCenterPt[1] - inkStrokeWidth / 2) / yScale + oldYrangeMin
+ };
+ const newPoints = ink.map(i => {
+ const pt = { X: i.X - inkCenterPt.X, Y: i.Y - inkCenterPt.Y };
+ const newX = Math.cos(angle) * pt.X - Math.sin(angle) * pt.Y * yScale / xScale;
+ const newY = Math.sin(angle) * pt.X * xScale / yScale + Math.cos(angle) * pt.Y;
+ return { X: newX + inkCenterPt.X, Y: newY + inkCenterPt.Y };
});
+ const doc = view.rootDoc;
doc.rotation = NumCast(doc.rotation) + angle;
return newPoints;
});
@@ -237,51 +186,104 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- moveControl = (deltaX: number, deltaY: number, controlIndex: number) =>
- this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
- const newPoints: { X: number, Y: number }[] = [];
+ moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number) =>
+ this.applyFunction(inkView, (view: DocumentView, ink: InkData, xScale: number, yScale: number) => {
const order = controlIndex % 4;
- for (var i = 0; i < ink.length; i++) {
+ const closed = InkingStroke.IsClosed(ink);
+
+ const newpts = ink.map((pt, i) => {
const leftHandlePoint = order === 0 && i === controlIndex + 1;
const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2;
if (controlIndex === i ||
leftHandlePoint ||
rightHandlePoint ||
(order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
+ ((order === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) ||
(order === 3 && i === controlIndex - 1) ||
(order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) ||
(order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) ||
((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))) {
- newPoints.push({ X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale });
- } else {
- newPoints.push({ X: ink[i].X, Y: ink[i].Y });
+ return ({ X: pt.X + deltaX / xScale, Y: pt.Y + deltaY / yScale });
}
- }
- return newPoints;
+ return pt;
+ });
+ return newpts;
})
+
+ public static nearestPtToStroke(ctrlPoints: { X: number, Y: number }[], refPt: { X: number, Y: number }, excludeSegs?: number[]) {
+ var distance = Number.MAX_SAFE_INTEGER;
+ var nearestT = -1;
+ var nearestSeg = -1;
+ var nearestPt = { X: 0, Y: 0 };
+ for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
+ if (excludeSegs?.includes(i)) continue;
+ const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
+ const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: refPt.X, y: refPt.Y });
+ if (point.t !== undefined) {
+ const dist = Math.sqrt((point.x - refPt.X) * (point.x - refPt.X) + (point.y - refPt.Y) * (point.y - refPt.Y));
+ if (dist < distance) {
+ distance = dist;
+ nearestT = point.t;
+ nearestSeg = i;
+ nearestPt = { X: point.x, Y: point.y };
+ }
+ }
+ }
+ return { distance, nearestT, nearestSeg, nearestPt };
+ }
+
+ /**
+ * Handles the movement/scaling of a control point.
+ */
+ snapControl = (inkView: DocumentView, controlIndex: number) => {
+ const inkDoc = inkView.rootDoc;
+ const ink = Cast(inkDoc.data, InkField)?.inkData;
+ if (ink) {
+ const closed = InkingStroke.IsClosed(ink);
+
+ // figure out which segments we don't want to snap to - avoid the dragged control point's segment and the next and prev segments (when they exist -- ie not for endpoints of unclosed curve)
+ const thisseg = Math.floor(controlIndex / 4) * 4;
+ const which = controlIndex % 4;
+ const nextseg = which > 1 && (closed || controlIndex < ink.length - 1) ? (thisseg + 4) % ink.length : -1;
+ const prevseg = which < 2 && (closed || controlIndex > 0) ? (thisseg - 4 + ink.length) % ink.length : -1;
+ const refPt = ink[controlIndex];
+ const { nearestPt } = InkStrokeProperties.nearestPtToStroke(ink, refPt, [thisseg, prevseg, nextseg]);
+
+ // nearestPt is in inkDoc coordinates -- we need to compute the distance in screen coordinates.
+ // so we scale the X & Y distances by the internal ink scale factor and then transform the final distance by the ScreenToLocal.Scale of the inkDoc itself.
+ const oldXrange = (xs => ({ coord: NumCast(inkDoc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X));
+ const oldYrange = (ys => ({ coord: NumCast(inkDoc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y));
+ const ptsXscale = ((NumCast(inkDoc._width) - NumCast(inkDoc.strokeWidth)) / ((oldXrange.max - oldXrange.min) || 1)) || 1;
+ const ptsYscale = ((NumCast(inkDoc._height) - NumCast(inkDoc.strokeWidth)) / ((oldYrange.max - oldYrange.min) || 1)) || 1;
+ const near = Math.sqrt((nearestPt.X - refPt.X) * (nearestPt.X - refPt.X) * ptsXscale * ptsXscale +
+ (nearestPt.Y - refPt.Y) * (nearestPt.Y - refPt.Y) * ptsYscale * ptsYscale);
+
+ if (near / (inkView.props.ScreenToLocalTransform().Scale || 1) < 10) {
+ return this.moveControlPtHandle(inkView, (nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex);
+ }
+ }
+ return false;
+ }
+
/**
* Snaps a control point with broken tangency back to synced rotation.
* @param handleIndexA The handle point that retains its current position.
* @param handleIndexB The handle point that is rotated to be 180 degrees from its opposite.
*/
- snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => {
- this.applyFunction((doc: Doc, ink: InkData) => {
- const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
- if (brokenIndices) {
- const newBrokenIndices = new List;
- brokenIndices.forEach(brokenIndex => {
- if (brokenIndex !== controlIndex) {
- newBrokenIndices.push(brokenIndex);
- }
- });
- doc.brokenInkIndices = newBrokenIndices;
+ snapHandleTangent = (inkView: DocumentView, controlIndex: number, handleIndexA: number, handleIndexB: number) => {
+ this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+ const doc = view.rootDoc;
+ const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"), []);
+ const ind = brokenIndices.findIndex(value => value === controlIndex);
+ if (ind !== -1) {
+ brokenIndices.splice(ind, 1);
const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]];
const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI);
const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint);
- const newHandleB = this.rotatePoint(handleB, controlPoint, angleDifference);
- ink[handleIndexB] = newHandleB;
- return ink;
+ const inkCopy = ink.slice(); // have to make a new copy of the array to keep from corrupting undo/redo. without slicing, the same array will be stored in each undo step meaning earlier undo steps will be inadvertently updated to store the latest value.
+ inkCopy[handleIndexB] = this.rotatePoint(handleB, controlPoint, angleDifference);
+ return inkCopy;
}
});
}
@@ -294,9 +296,7 @@ export class InkStrokeProperties {
const rotatedTarget = { X: target.X - origin.X, Y: target.Y - origin.Y };
const newX = Math.cos(angle) * rotatedTarget.X - Math.sin(angle) * rotatedTarget.Y;
const newY = Math.sin(angle) * rotatedTarget.X + Math.cos(angle) * rotatedTarget.Y;
- rotatedTarget.X = newX + origin.X;
- rotatedTarget.Y = newY + origin.Y;
- return rotatedTarget;
+ return { X: newX + origin.X, Y: newY + origin.Y };
}
/**
@@ -307,11 +307,11 @@ export class InkStrokeProperties {
angleBetweenTwoVectors = (vectorA: PointData, vectorB: PointData) => {
const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y);
const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y);
+ if (magnitudeA === 0 || magnitudeB === 0) return 0;
// Normalizing the vectors.
vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA };
vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB };
- const dotProduct = vectorB.X * vectorA.X + vectorB.Y * vectorA.Y;
- return Math.acos(dotProduct);
+ return Math.acos(vectorB.X * vectorA.X + vectorB.Y * vectorA.Y);
}
/**
@@ -333,20 +333,24 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
- this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
+ moveTangentHandle = (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
+ this.applyFunction(inkView, (view: DocumentView, ink: InkData, xScale: number, yScale: number) => {
+ const doc = view.rootDoc;
+ const closed = InkingStroke.IsClosed(ink);
const oldHandlePoint = ink[handleIndex];
- let oppositeHandlePoint = ink[oppositeHandleIndex];
+ const oppositeHandlePoint = ink[oppositeHandleIndex];
const controlPoint = ink[controlIndex];
const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale };
- ink[handleIndex] = newHandlePoint;
+ const inkCopy = ink.slice();
+ inkCopy[handleIndex] = newHandlePoint;
const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
+ const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1;
// Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
- if ((!brokenIndices || !brokenIndices?.includes(controlIndex)) && handleIndex !== 1 && handleIndex !== ink.length - 2) {
+ if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) &&
+ (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
- oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
- ink[oppositeHandleIndex] = oppositeHandlePoint;
+ inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
}
- return ink;
+ return inkCopy;
})
} \ No newline at end of file
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
new file mode 100644
index 000000000..f88a20448
--- /dev/null
+++ b/src/client/views/InkTangentHandles.tsx
@@ -0,0 +1,131 @@
+import React = require("react");
+import { action } from "mobx";
+import { observer } from "mobx-react";
+import { Doc } from "../../fields/Doc";
+import { HandleLine, HandlePoint, InkData } from "../../fields/InkField";
+import { List } from "../../fields/List";
+import { listSpec } from "../../fields/Schema";
+import { Cast } from "../../fields/Types";
+import { emptyFunction, setupMoveUpEvents } from "../../Utils";
+import { Transform } from "../util/Transform";
+import { UndoManager } from "../util/UndoManager";
+import { Colors } from "./global/globalEnums";
+import { InkingStroke } from "./InkingStroke";
+import { InkStrokeProperties } from "./InkStrokeProperties";
+import { DocumentView } from "./nodes/DocumentView";
+
+export interface InkHandlesProps {
+ inkDoc: Doc;
+ inkView: DocumentView;
+ screenCtrlPoints: InkData;
+ screenSpaceLineWidth: number;
+ ScreenToLocalTransform: () => Transform;
+}
+
+@observer
+export class InkTangentHandles extends React.Component<InkHandlesProps> {
+ /**
+ * Handles the movement of a selected handle point when the user clicks and drags.
+ * @param handleNum The index of the currently selected handle point.
+ */
+ onHandleDown = (e: React.PointerEvent, handleIndex: number): void => {
+ if (InkStrokeProperties.Instance) {
+ var controlUndo: UndoManager.Batch | undefined;
+ const screenScale = this.props.ScreenToLocalTransform().Scale;
+ const order = handleIndex % 4;
+ const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
+ const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length;
+ const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length;
+ setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
+ if (!controlUndo) controlUndo = UndoManager.StartBatch("DocDecs move tangent");
+ if (e.altKey) this.onBreakTangent(controlIndex);
+ InkStrokeProperties.Instance?.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
+ return false;
+ }, () => {
+ controlUndo?.end();
+ UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+ }, emptyFunction
+ );
+ }
+ }
+
+ /**
+ * Breaks tangent handle movement when ‘Alt’ key is held down. Adds the current handle index and
+ * its matching (opposite) handle to a list of broken handle indices.
+ * @param handleNum The index of the currently selected handle point.
+ */
+ @action
+ onBreakTangent = (controlIndex: number) => {
+ const closed = InkingStroke.IsClosed(this.props.screenCtrlPoints);
+ const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
+ if (!brokenIndices?.includes(controlIndex) &&
+ ((controlIndex > 0 && controlIndex < this.props.screenCtrlPoints.length - 1) || closed)) {
+ if (brokenIndices) brokenIndices.push(controlIndex);
+ else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
+ }
+ }
+
+ render() {
+ const formatInstance = InkStrokeProperties.Instance;
+ if (!formatInstance) return (null);
+
+ // Accessing the current ink's data and extracting all handle points and handle lines.
+ const data = this.props.screenCtrlPoints;
+ const tangentHandles: HandlePoint[] = [];
+ const tangentLines: HandleLine[] = [];
+ const closed = InkingStroke.IsClosed(data);
+ if (data.length >= 4) {
+ for (let i = 0; i <= data.length - 4; i += 4) {
+ tangentHandles.push({ ...data[i + 1], I: i + 1, dot1: i, dot2: i === 0 ? (closed ? data.length - 1 : i) : i - 1 });
+ tangentHandles.push({ ...data[i + 2], I: i + 2, dot1: i + 3, dot2: i === data.length ? (closed ? (i + 4) % data.length : i + 3) : i + 4 });
+ }
+ // Adding first and last (single) handle lines.
+ if (closed) {
+ tangentLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: data.length - 1 });
+ }
+ else {
+ tangentLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 });
+ tangentLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 });
+ }
+ for (let i = 2; i < data.length - 4; i += 4) {
+ tangentLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 });
+ }
+ }
+ const screenSpaceLineWidth = this.props.screenSpaceLineWidth;
+
+ return (
+ <>
+ {tangentHandles.map((pts, i) =>
+ <svg height="10" width="10" key={`hdl${i}`}>
+ <circle
+ cx={pts.X}
+ cy={pts.Y}
+ r={screenSpaceLineWidth * 2}
+ fill={Colors.MEDIUM_BLUE}
+ strokeWidth={1}
+ stroke={Colors.MEDIUM_BLUE}
+ onPointerDown={e => this.onHandleDown(e, pts.I)}
+ pointerEvents="all"
+ cursor="default"
+ display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />
+ </svg>)}
+ {tangentLines.map((pts, i) => {
+ const tangentLine = (x1: number, y1: number, x2: number, y2: number) =>
+ <line
+ x1={x1}
+ y1={y1}
+ x2={x2}
+ y2={y2}
+ stroke={Colors.MEDIUM_BLUE}
+ strokeDasharray={"1 1"}
+ strokeWidth={1}
+ display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />;
+ return <svg height="100" width="100" key={`line${i}`}>
+ {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)}
+ {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)}
+ </svg>;
+ })}
+ </>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index ca39bdaa1..d312331d0 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -5,39 +5,46 @@ import { Doc } from "../../fields/Doc";
import { documentSchema } from "../../fields/documentSchemas";
import { InkData, InkField, InkTool } from "../../fields/InkField";
import { makeInterface } from "../../fields/Schema";
-import { Cast, NumCast, StrCast } from "../../fields/Types";
+import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types";
import { TraceMobx } from "../../fields/util";
import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../Utils";
import { CognitiveServices } from "../cognitive_services/CognitiveServices";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { InteractionUtils } from "../util/InteractionUtils";
-import { Scripting } from "../util/Scripting";
+import { SnappingManager } from "../util/SnappingManager";
import { ContextMenu } from "./ContextMenu";
import { ViewBoxBaseComponent } from "./DocComponent";
import { Colors } from "./global/globalEnums";
-import { InkControls } from "./InkControls";
-import { InkHandles } from "./InkHandles";
-import { GestureOverlay } from "./GestureOverlay";
-import { isThisTypeNode } from "typescript";
+import { InkControlPtHandles } from "./InkControlPtHandles";
import "./InkStroke.scss";
import { InkStrokeProperties } from "./InkStrokeProperties";
+import { InkTangentHandles } from "./InkTangentHandles";
import { FieldView, FieldViewProps } from "./nodes/FieldView";
+import Color = require("color");
type InkDocument = makeInterface<[typeof documentSchema]>;
const InkDocument = makeInterface(documentSchema);
@observer
export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
+ public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
static readonly MaskDim = 50000;
+ public static IsClosed(inkData: InkData) {
+ return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
+ }
@observable private _properties?: InkStrokeProperties;
_handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
_selDisposer: IReactionDisposer | undefined;
+ @observable _nearestT: number | undefined;
+ @observable _nearestSeg: number | undefined;
+ @observable _nearestScrPt: { X: number, Y: number } | undefined;
+ @observable _inkSamplePts: { X: number, Y: number }[] | undefined;
+
constructor(props: FieldViewProps & InkDocument) {
super(props);
this._properties = InkStrokeProperties.Instance;
- // this._previousColor = ActiveInkColor();
}
componentDidMount() {
@@ -49,10 +56,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
this._selDisposer?.();
}
- public static LayoutString(fieldStr: string) {
- return FieldView.LayoutString(InkingStroke, fieldStr);
- }
-
analyzeStrokes() {
const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
@@ -65,13 +68,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
inkDoc.color = "#9b9b9bff";
inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
});
-
- onClick = (e: React.MouseEvent) => {
- if (this._handledClick) {
- e.stopPropagation(); //stop the event so that docView won't open the lightbox
- }
- }
-
/**
* Handles the movement of the entire ink object when the user clicks and drags.
*/
@@ -83,7 +79,10 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick;
if (doubleTap && this._properties) {
this._properties._controlButton = true;
+ InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1);
this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
+ } else if (this._properties?._controlButton) {
+ this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
}
}), this._properties?._controlButton, this._properties?._controlButton
);
@@ -106,13 +105,10 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
// this._previousColor = ActiveInkColor();
SetActiveInkColor("rgba(245, 230, 95, 0.75)");
}
- // } else {
- // SetActiveInkColor(this._previousColor);
- // }
}
inkScaledData = () => {
- const inkData: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
+ const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1);
const inkTop = Math.min(...inkData.map(p => p.Y)) - inkStrokeWidth / 2;
const inkBottom = Math.max(...inkData.map(p => p.Y)) + inkStrokeWidth / 2;
@@ -127,76 +123,94 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
inkLeft,
inkWidth,
inkHeight,
- inkScaleX: inkHeight === inkStrokeWidth ? 1 : (this.props.PanelWidth() - inkStrokeWidth) / (inkWidth - inkStrokeWidth),
- inkScaleY: inkWidth === inkStrokeWidth ? 1 : (this.props.PanelHeight() - inkStrokeWidth) / (inkHeight - inkStrokeWidth)
+ inkScaleX: ((this.props.PanelWidth() - inkStrokeWidth) / ((inkWidth - inkStrokeWidth) || 1) || 1),
+ inkScaleY: ((this.props.PanelHeight() - inkStrokeWidth) / ((inkHeight - inkStrokeWidth) || 1) || 1)
};
}
+ @action
+ onPointerMove = (e: React.PointerEvent) => {
+ const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
+ const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint(
+ (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2,
+ (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] }));
+ const { distance, nearestT, nearestSeg, nearestPt } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
+
+ this._nearestT = nearestT;
+ this._nearestSeg = nearestSeg;
+ this._nearestScrPt = nearestPt;
+ }
+
+
+ nearestScreenPt = () => this._nearestScrPt;
componentUI = (boundsLeft: number, boundsTop: number) => {
const inkDoc = this.props.Document;
const screenSpaceCenterlineStrokeWidth = 3; // the width of the blue line widget that shows the centerline of the ink stroke
const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
const screenInkWidth = this.props.ScreenToLocalTransform().inverse().transformDirection(inkStrokeWidth, inkStrokeWidth);
- const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint(point.X, point.Y)).map(p => ({ X: p[0], Y: p[1] }));
- const screenTop = Math.min(...screenPts.map(p => p.Y)) - screenInkWidth[0] / 2;
- const screenLeft = Math.min(...screenPts.map(p => p.X)) - screenInkWidth[0] / 2;
- const screenOrigin = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
-
- const screenSpaceSamplePoints = InteractionUtils.CreatePoints(screenPts, screenLeft, screenTop, StrCast(inkDoc.strokeColor, "none"), screenInkWidth[0], screenSpaceCenterlineStrokeWidth,
- StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker),
- StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", this.props.isSelected() && inkStrokeWidth <= 5, false);
- const inkSpaceSamplePoints = InteractionUtils.CreatePoints(inkData, inkLeft, inkTop, StrCast(inkDoc.strokeColor, "none"), inkStrokeWidth, screenSpaceCenterlineStrokeWidth,
- StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker),
- StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), 1, 1, "", "none", this.props.isSelected() && inkStrokeWidth <= 5, false);
+ const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint(
+ (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2,
+ (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] }));
+ const screenHdlPts = screenPts;
- return <div className="inkstroke-UI" style={{
- left: screenOrigin[0],
- top: screenOrigin[1],
- clip: `rect(${boundsTop - screenOrigin[1]}px, 10000px, 10000px, ${boundsLeft - screenOrigin[0]}px)`
+ const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
+ const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
+ return SnappingManager.GetIsDragging() ? (null) : <div className="inkstroke-UI" style={{
+ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)`
}} >
- {InteractionUtils.CreatePolyline(screenPts, screenLeft, screenTop, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth,
- StrCast(inkDoc.strokeBezier), StrCast(inkDoc.fillColor, "none"),
- StrCast(inkDoc.strokeStartMarker), StrCast(inkDoc.strokeEndMarker),
- StrCast(inkDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false)}
- {this._properties?._controlButton ?
+ {!this._properties?._controlButton ? (null) :
<>
- <InkControls
+ {InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth,
+ StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier),
+ "none", startMarker, endMarker, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)}
+ <InkControlPtHandles
+ inkView={this.props.docViewPath().lastElement()}
inkDoc={inkDoc}
inkCtrlPoints={inkData}
- screenCtrlPoints={screenPts}
- inkStrokeSamplePts={inkSpaceSamplePoints}
- screenStrokeSamplePoints={screenSpaceSamplePoints}
- format={[screenLeft, screenTop, inkScaleX, inkScaleY, screenInkWidth[0], screenSpaceCenterlineStrokeWidth]}
+ screenCtrlPoints={screenHdlPts}
+ nearestScreenPt={this.nearestScreenPt}
+ screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth}
ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
- <InkHandles
+ <InkTangentHandles
+ inkView={this.props.docViewPath().lastElement()}
inkDoc={inkDoc}
- data={screenPts}
- format={[screenLeft, screenTop, inkScaleX, inkScaleY, screenInkWidth[0], screenSpaceCenterlineStrokeWidth]}
+ screenCtrlPoints={screenHdlPts}
+ screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth}
ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
- </> : ""}
+ </>}
</div>;
}
render() {
TraceMobx();
- // Extracting the ink data and formatting information of the current ink stroke.
- const inkDoc: Doc = this.layoutDoc;
-
const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY, inkWidth, inkHeight } = this.inkScaledData();
- const strokeColor = StrCast(this.layoutDoc.color, "");
- const dotsize = Math.max(inkWidth * inkScaleX, inkHeight * inkScaleY) / 40;
+ const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
+ const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
+ const closed = InkingStroke.IsClosed(inkData);
+ const fillColor = StrCast(this.layoutDoc.fillColor, "transparent");
+ const strokeColor = !closed && fillColor && fillColor !== "transparent" ? fillColor : StrCast(this.layoutDoc.color);
// Visually renders the polygonal line made by the user.
- const inkLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, strokeColor, inkStrokeWidth, inkStrokeWidth, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker),
- StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false);
+ const inkLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, strokeColor, inkStrokeWidth, inkStrokeWidth,
+ StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
+ StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
+ StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false);
// Thin blue line indicating that the current ink stroke is selected.
// const selectedLine = InteractionUtils.CreatePolyline(data, left, top, Colors.MEDIUM_BLUE, strokeWidth, strokeWidth / 6, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
// StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", 1.0, false);
// Invisible polygonal line that enables the ink to be selected by the user.
- const clickableLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, "transparent", inkStrokeWidth, inkStrokeWidth + 15, StrCast(this.layoutDoc.strokeBezier),
- StrCast(this.layoutDoc.fillColor, "none"), "none", "none", undefined, inkScaleX, inkScaleY, "", this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted", 0.0, true);
+ const highlightIndex = BoolCast(this.props.Document.isLinkButton) && Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
+ const highlightColor = !highlightIndex ?
+ StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== "transparent" ? StrCast(this.layoutDoc.color, "transparent") : "transparent") :
+ ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex];
+
+ const clickableLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor,
+ inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
+ StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
+ StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
+ undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, false);
// Set of points rendered upon the ink that can be added if a user clicks on one.
return (
@@ -206,36 +220,21 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
overflow: "visible",
+ cursor: this.props.isSelected() ? "default" : undefined
}}
+ onPointerLeave={action(e => this._nearestScrPt = undefined)}
+ onPointerMove={this.props.isSelected() ? this.onPointerMove : undefined}
onPointerDown={this.onPointerDown}
- onClick={this.onClick}
+ onClick={e => this._handledClick && e.stopPropagation()}
onContextMenu={() => {
const cm = ContextMenu.Instance;
- if (cm) {
- !Doc.UserDoc().noviceMode && cm.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" });
- cm.addItem({ description: "Toggle Mask", event: () => InkingStroke.toggleMask(this.rootDoc), icon: "paint-brush" });
- cm.addItem({ description: "Edit Points", event: action(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" });
- }
+ !Doc.UserDoc().noviceMode && cm?.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" });
+ cm?.addItem({ description: "Toggle Mask", event: () => InkingStroke.toggleMask(this.rootDoc), icon: "paint-brush" });
+ cm?.addItem({ description: "Edit Points", event: action(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" });
}}
>
-
{clickableLine}
{inkLine}
- {/* {this.props.isSelected() ? selectedLine : ""} */}
- {/* {this.props.isSelected() && this._properties?._controlButton ?
- <>
- <InkControls
- inkDoc={inkDoc}
- data={inkData}
- addedPoints={addedPoints}
- format={[inkLeft, inkTop, inkScaleX, inkScaleY, inkStrokeWidth]}
- ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
- <InkHandles
- inkDoc={inkDoc}
- data={inkData}
- format={[inkLeft, inkTop, inkScaleX, inkScaleY, inkStrokeWidth]}
- ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
- </> : ""} */}
</svg>
);
}
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index ec30a6a5d..ec3bf6c18 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -214,7 +214,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
LightboxView.SetLightboxDoc(undefined);
}
}} >
-
+
<div className="lightboxView-contents" style={{
left: this.leftBorder,
top: this.topBorder,
@@ -237,7 +237,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
DataDoc={undefined}
LayoutTemplate={LightboxView.LightboxDocTemplate}
addDocument={undefined}
- fitContentsToDoc={this.fitToBox}
+ // fitContentsToDoc={this.fitToBox} // bcz: why do we want this? when we initially open a colletion, we shrinkwrap it which allows for user navigation. if we later encounter a collection, it's not clear to me that we want to make it either shrinkwrap or fitContents...
isDocumentActive={returnFalse}
isContentActive={returnTrue}
addDocTab={this.addDocTab}
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index 4f871f5ec..15cd2c144 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -41,7 +41,7 @@
}
.mainView-container,
-.mainView-container-dark {
+.mainView-container-Dark {
width: 100%;
height: 100%;
position: absolute;
@@ -59,13 +59,17 @@
.mainView-container {
color: $dark-gray;
+ .lm_goldenlayout {
+ background: $medium-gray;
+ }
+
.lm_title {
background: $light-gray;
color: $dark-gray;
}
}
-.mainView-container-dark {
+.mainView-container-Dark {
color: $light-gray;
.lm_goldenlayout {
@@ -91,7 +95,7 @@
.contextMenu-cont,
.contextMenu-item {
- background: $medium-gray;
+ background: $dark-gray;
}
.contextMenu-item:hover {
@@ -109,9 +113,9 @@
.properties-container {
height: 100%;
- position: relative;
- left: 100%;
- top: calc(-100% - 36px);
+ position: absolute;
+ right: 0;
+ top: 0;
z-index: 3000;
}
@@ -144,7 +148,7 @@
}
}
-.mainView-innerContent, .mainView-innerContent-dark {
+.mainView-innerContent, .mainView-innerContent-Dark {
display: contents;
flex-direction: row;
position: relative;
@@ -166,44 +170,51 @@
position: absolute;
z-index: 2;
background-color: $light-gray;
+
.editable-title {
background-color: $light-gray;
}
}
}
+
.mainView-libraryHandle {
background-color: $light-gray;
}
-.mainView-innerContent-dark
+.mainView-innerContent-Dark
{
.propertiesView {
background-color: #252525;
+
input {
background-color: $medium-gray;
}
- .propertiesView-sharingTable
- {
+
+ .propertiesView-sharingTable {
background-color: $medium-gray;
}
+
.editable-title {
background-color: $medium-gray;
}
+
.propertiesView-field {
background-color: $medium-gray;
}
}
+
.mainView-propertiesDragger,
.mainView-libraryHandle {
background: #353535;
}
}
-.mainView-container-dark {
+.mainView-container-Dark {
.contextMenu-cont {
background: $medium-gray;
color: $white;
+
input::placeholder {
- color:$white;
+ color: $white;
}
}
}
@@ -298,9 +309,8 @@
width: var(--flyoutHandleWidth);
height: 55px;
top: 50%;
- left: -10px;
border-radius: 8px;
- position: relative;
+ position: absolute;
z-index: 41; // lm_maximised has a z-index of 40 and this needs to be above that
touch-action: none;
cursor: grab;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ea48a72b5..7ec7277a9 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -12,7 +12,7 @@ import { Doc, DocListCast, Opt } from '../../fields/Doc';
import { List } from '../../fields/List';
import { PrefetchProxy } from '../../fields/Proxy';
import { ScriptField } from '../../fields/ScriptField';
-import { BoolCast, PromiseValue, StrCast } from '../../fields/Types';
+import { PromiseValue, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
@@ -26,7 +26,7 @@ import { HistoryUtil } from '../util/History';
import { Hypothesis } from '../util/HypothesisUtils';
import { Scripting } from '../util/Scripting';
import { SelectionManager } from '../util/SelectionManager';
-import { SettingsManager } from '../util/SettingsManager';
+import { ColorScheme, SettingsManager } from '../util/SettingsManager';
import { SharingManager } from '../util/SharingManager';
import { SnappingManager } from '../util/SnappingManager';
import { Transform } from '../util/Transform';
@@ -88,7 +88,7 @@ export class MainView extends React.Component {
@computed private get topOfMainDocContent() { return this.topOfMainDoc + this.dashboardTabHeight; }
@computed private get leftScreenOffsetOfMainDocView() { return this.leftMenuWidth() - 2; }
@computed private get userDoc() { return Doc.UserDoc(); }
- @computed private get darkScheme() { return BoolCast(CurrentUserUtils.ActiveDashboard?.darkScheme); }
+ @computed private get colorScheme() { return StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme); }
@computed private get mainContainer() { return this.userDoc ? CurrentUserUtils.ActiveDashboard : CurrentUserUtils.GuestDashboard; }
@computed public get mainFreeform(): Opt<Doc> { return (docs => (docs?.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); }
@@ -101,7 +101,7 @@ export class MainView extends React.Component {
propertiesWidth = () => Math.max(0, Math.min(this._dashUIWidth - 50, CurrentUserUtils.propertiesWidth || 0));
propertiesHeight = () => this._dashUIHeight;
mainDocViewWidth = () => this._dashUIWidth - this.propertiesWidth() - this.leftMenuWidth();
- mainDocViewHeight = () => this._dashUIHeight - this.topMenuHeight();
+ mainDocViewHeight = () => this._dashUIHeight;
componentDidMount() {
document.getElementById("root")?.addEventListener("scroll", e => ((ele) => ele.scrollLeft = ele.scrollTop = 0)(document.getElementById("root")!));
@@ -214,6 +214,7 @@ export class MainView extends React.Component {
}
}
}, false);
+ document.oncontextmenu = () => false;
}
initAuthenticationRouters = async () => {
@@ -425,24 +426,23 @@ export class MainView extends React.Component {
}
@computed get mainInnerContent() {
- const width = this.propertiesWidth() + this._leftMenuFlyoutWidth + this.leftMenuWidth();
- const transform = this._leftMenuFlyoutWidth ? 'translate(-28px, 0px)' : undefined;
+ const leftMenuFlyoutWidth = this._leftMenuFlyoutWidth + this.leftMenuWidth();
+ const width = this.propertiesWidth() + leftMenuFlyoutWidth;
return <>
{this.leftMenuPanel}
- <div key="inner" className={`mainView-innerContent${this.darkScheme ? "-dark" : ""}`}>
+ <div key="inner" className={`mainView-innerContent${this.colorScheme}`}>
{this.flyout}
- <div className="mainView-libraryHandle" style={{ display: !this._leftMenuFlyoutWidth ? "none" : undefined }} onPointerDown={this.onFlyoutPointerDown} >
- <FontAwesomeIcon icon="chevron-left" color={this.darkScheme ? "white" : "black"} style={{ opacity: "50%" }} size="sm" />
+ <div className="mainView-libraryHandle" style={{ left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? "none" : undefined }} onPointerDown={this.onFlyoutPointerDown} >
+ <FontAwesomeIcon icon="chevron-left" color={this.colorScheme === ColorScheme.Dark ? "white" : "black"} style={{ opacity: "50%" }} size="sm" />
</div>
- <div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)`, transform: transform }}>
- <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} />
+ <div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)` }}>
{this.dockingContent}
- <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this._leftMenuFlyoutWidth ? 0 : this.propertiesWidth() - 1 }}>
- <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? "chevron-left" : "chevron-right"} color={this.darkScheme ? Colors.WHITE : Colors.BLACK} size="sm" />
+ <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1 }}>
+ <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? "chevron-left" : "chevron-right"} color={this.colorScheme === ColorScheme.Dark ? Colors.WHITE : Colors.BLACK} size="sm" />
</div>
- <div className="properties-container">
+ <div className="properties-container" style={{ width: this.propertiesWidth() }}>
{this.propertiesWidth() < 10 ? (null) : <PropertiesView styleProvider={DefaultStyleProvider} width={this.propertiesWidth()} height={this.propertiesHeight()} />}
</div>
</div>
@@ -458,19 +458,25 @@ export class MainView extends React.Component {
this._dashUIHeight = r.getBoundingClientRect().height;
})).observe(r);
}} style={{
- color: this.darkScheme ? "rgb(205,205,205)" : "black",
- height: `calc(100% - ${this.topOfDashUI}px)`,
+ color: this.colorScheme === ColorScheme.Dark ? "rgb(205,205,205)" : "black",
+ height: `calc(100% - ${this.topOfDashUI + this.topMenuHeight()}px)`,
width: "100%",
}} >
{this.mainInnerContent}
</div>;
}
+
expandFlyout = action((button: Doc) => {
+ // bcz: What's going on here!?
+ // Chrome(not firefox) seems to have a bug when the flyout expands and there's a zoomed freeform tab. All of the div below the CollectionFreeFormView's main div
+ // generate the wrong value from getClientRectangle() -- specifically they return an 'x' that is the flyout's width greater than it should be.
+ // interactively adjusting the flyout fixes the problem. So does programmatically changing the value after a timeout to something *fractionally* different (ie, 1.5, not 1);)
this._leftMenuFlyoutWidth = (this._leftMenuFlyoutWidth || 250);
+ setTimeout(action(() => this._leftMenuFlyoutWidth += 0.5), 0);
+
this._sidebarContent.proto = button.target as any;
this.LastButton = button;
- console.log(button.title);
});
closeFlyout = action(() => {
@@ -600,7 +606,7 @@ export class MainView extends React.Component {
}
render() {
- return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")}
+ return (<div className={`mainView-container${this.colorScheme}`}
onScroll={() => ((ele) => ele.scrollTop = ele.scrollLeft = 0)(document.getElementById("root")!)}
ref={r => {
r && new _global.ResizeObserver(action(() => { this._windowWidth = r.getBoundingClientRect().width; this._windowHeight = r.getBoundingClientRect().height; })).observe(r);
@@ -618,6 +624,9 @@ export class MainView extends React.Component {
{LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
{DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} changeFlyout={emptyFunction} /> : (null)}
{LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : (null)}
+ <div style={{ position: "relative", display: LightboxView.LightboxDoc ? "none" : undefined, zIndex: 2001 }} >
+ <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} />
+ </div>
<GestureOverlay >
{this.mainDashboardArea}
</GestureOverlay>
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 26e76090a..563261dec 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -31,7 +31,7 @@ export interface MarqueeAnnotatorProps {
annotationLayer: HTMLDivElement;
addDocument: (doc: Doc) => boolean;
getPageFromScroll?: (top: number) => number;
- finishMarquee: (x?: number, y?: number) => void;
+ finishMarquee: (x?: number, y?: number, PointerEvent?: PointerEvent) => void;
anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
}
@observer
@@ -222,10 +222,10 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
if (AnchorMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up
this.highlight("rgba(245, 230, 95, 0.75)", false); // yellowish highlight color for highlighted text (should match AnchorMenu's highlight color)
}
- this.props.finishMarquee();
+ this.props.finishMarquee(undefined, undefined, e);
} else {
runInAction(() => this._width = this._height = 0);
- this.props.finishMarquee(cliX, cliY);
+ this.props.finishMarquee(cliX, cliY, e);
}
}
diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss
index de9bd69c4..60b7d14a0 100644
--- a/src/client/views/PreviewCursor.scss
+++ b/src/client/views/PreviewCursor.scss
@@ -1,4 +1,5 @@
+.previewCursor-Dark,
.previewCursor {
color: black;
position: absolute;
@@ -8,4 +9,8 @@
pointer-events: none;
opacity: 1;
z-index: 1001;
+}
+
+.previewCursor-Dark {
+ color: white;
} \ No newline at end of file
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 2b82ef475..ef1360ef1 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
import "normalize.css";
import * as React from 'react';
import { Doc } from '../../fields/Doc';
-import { Cast, NumCast } from '../../fields/Types';
+import { Cast, NumCast, StrCast } from '../../fields/Types';
import { DocServer } from '../DocServer';
import { Docs, DocUtils } from '../documents/Documents';
import { CurrentUserUtils } from '../util/CurrentUserUtils';
@@ -158,7 +158,7 @@ export class PreviewCursor extends React.Component<{}> {
}
render() {
return (!PreviewCursor._clickPoint || !PreviewCursor.Visible) ? (null) :
- <div className="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={e => e?.focus()}
+ <div className={`previewCursor${StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme)}`} onBlur={this.onBlur} tabIndex={0} ref={e => e?.focus()}
style={{ transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)` }}>
I
</div >;
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index b29953b19..62f6e388f 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -69,8 +69,8 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
if (DocListCast(this.props.rootDoc[this.sidebarKey]).includes(doc)) {
if (this.props.layoutDoc[this.filtersKey]) {
this.props.layoutDoc[this.filtersKey] = new List<string>();
- return true;
}
+ return true;
}
return false;
}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index c5614506b..ed841d0f5 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -1,27 +1,26 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Colors } from './global/globalEnums';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { runInAction, action } from 'mobx';
+import { action, runInAction } from 'mobx';
import { Doc, Opt, StrListCast } from "../../fields/Doc";
import { List } from '../../fields/List';
import { listSpec } from '../../fields/Schema';
import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types";
+import { lightOrDark, DashColor } from '../../Utils';
import { DocumentType } from '../documents/DocumentTypes';
import { CurrentUserUtils } from '../util/CurrentUserUtils';
+import { ColorScheme } from '../util/SettingsManager';
import { SnappingManager } from '../util/SnappingManager';
-import { UndoManager, undoBatch } from '../util/UndoManager';
+import { undoBatch, UndoManager } from '../util/UndoManager';
import { CollectionViewType } from './collections/CollectionView';
-import "./collections/TreeView.scss";
+import { Colors } from './global/globalEnums';
import { MainView } from './MainView';
import { DocumentViewProps } from "./nodes/DocumentView";
import { FieldViewProps } from './nodes/FieldView';
-import "./nodes/FilterBox.scss";
+import { SliderBox } from './nodes/SliderBox';
import "./StyleProvider.scss";
import React = require("react");
-import Color = require('color');
-import { lightOrDark } from '../../Utils';
export enum StyleLayers {
Background = "background"
@@ -49,7 +48,7 @@ export enum StyleProp {
FontSize = "fontSize", // size of text font
}
-function darkScheme() { return BoolCast(CurrentUserUtils.ActiveDashboard?.darkScheme); }
+function darkScheme() { return CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark; }
function toggleBackground(doc: Doc) {
UndoManager.RunInBatch(() => runInAction(() => {
@@ -109,7 +108,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case StyleProp.Hidden: return BoolCast(doc?._hidden);
case StyleProp.BorderRounding: return StrCast(doc?.[fieldKey + "borderRounding"], doc?._viewType === CollectionViewType.Pile ? "50%" : "");
case StyleProp.TitleHeight: return 15;
- case StyleProp.BorderPath: return comicStyle() && props?.renderDepth ? { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, .08), width: 3 } : { path: undefined, width: 0 };
+ case StyleProp.BorderPath: return comicStyle() && props?.renderDepth && doc?.type !== DocumentType.INK ? { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, .08), width: 3 } : { path: undefined, width: 0 };
case StyleProp.JitterRotation: return comicStyle() ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0;
case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) ||
doc?.type === DocumentType.RTF) && showTitle() && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0;
@@ -136,7 +135,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case DocumentType.SCREENSHOT:
case DocumentType.VID: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break;
case DocumentType.COL:
- if (StrCast(Doc.LayoutField(doc)).includes("SliderBox")) break;
+ if (StrCast(Doc.LayoutField(doc)).includes(SliderBox.name)) break;
docColor = docColor ||
(doc?._isGroup ? "#00000004" : // very faint highlight to show bounds of group
(doc?._viewType === CollectionViewType.Pile || Doc.IsSystem(doc) ? (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY) : // system docs (seen in treeView) get a grayish background
@@ -144,18 +143,18 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
doc.annotationOn ? "#00000015" : // faint interior for collections on PDFs, images, etc
StrCast((props?.renderDepth || 0) > 0 ?
Doc.UserDoc().activeCollectionNestedBackground :
- Doc.UserDoc().activeCollectionBackground)));
+ Doc.UserDoc().activeCollectionBackground ?? (darkScheme() ? Colors.BLACK : Colors.WHITE))));
break;
//if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)";
default: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); break;
}
- if (docColor && (!doc || props?.layerProvider?.(doc) === false)) docColor = Color(docColor.toLowerCase()).fade(0.5).toString();
+ if (docColor && (!doc || props?.layerProvider?.(doc) === false)) docColor = DashColor(docColor).fade(0.5).toString();
return docColor;
}
case StyleProp.BoxShadow: {
if (!doc || opacity() === 0) return undefined; // if it's not visible, then no shadow)
- if (doc?.isLinkButton && doc.type !== DocumentType.LINK) return StrCast(doc?._linkButtonShadow, "lightblue 0em 0em 1em");
+ if (doc?.isLinkButton && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, "lightblue 0em 0em 1em");
switch (doc?.type) {
case DocumentType.COL:
@@ -183,13 +182,11 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
if (doc?.type !== DocumentType.INK && layer === true) return "all";
return undefined;
case StyleProp.Decorations:
- // if (isFooter)
-
if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform) {
return doc && (isBackground() || selected) && (props?.renderDepth || 0) > 0 &&
((doc.type === DocumentType.COL && doc._viewType !== CollectionViewType.Pile) || [DocumentType.RTF, DocumentType.IMG, DocumentType.INK].includes(doc.type as DocumentType)) ?
<div className="styleProvider-lock" onClick={() => toggleBackground(doc)}>
- <FontAwesomeIcon icon={isBackground() ? "unlock" : "lock"} style={{ color: isBackground() ? "red" : undefined }} size="lg" />
+ <FontAwesomeIcon icon={isBackground() ? "lock" : "unlock"} style={{ color: isBackground() ? "red" : undefined }} size="lg" />
</div>
: (null);
}
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 77e7b86ea..b2ee33807 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -65,6 +65,29 @@
display: inline;
}
+.empty-tabs-message {
+ position: absolute;
+ width: 100%;
+ z-index: 1;
+ top: 50%;
+ z-index: 1;
+ text-align: center;
+ font-size: 18;
+ color: $dark-gray;
+
+ img {
+ position: relative;
+ top: -1px;
+ margin: 0 5px;
+ }
+}
+
+.lm_header,
+.lm_items {
+ z-index: 2;
+ position: relative;
+}
+
.lm_drag_tab {
padding: 0;
width: 15px !important;
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
deleted file mode 100644
index 6a22acae8..000000000
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ /dev/null
@@ -1,575 +0,0 @@
-import React = require("react");
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked } from "mobx";
-import { observer } from "mobx-react";
-import Measure from "react-measure";
-import { Resize } from "react-table";
-import "react-table/react-table.css";
-import { Doc, Opt } from "../../../fields/Doc";
-import { List } from "../../../fields/List";
-import { listSpec } from "../../../fields/Schema";
-import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField";
-import { Cast, NumCast } from "../../../fields/Types";
-import { TraceMobx } from "../../../fields/util";
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils";
-import { DocUtils } from "../../documents/Documents";
-import { SelectionManager } from "../../util/SelectionManager";
-import { SnappingManager } from "../../util/SnappingManager";
-import { Transform } from "../../util/Transform";
-import { undoBatch } from "../../util/UndoManager";
-import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../views/global/globalCssVariables.scss';
-import { SchemaTable } from "../collections/collectionSchema/SchemaTable";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import '../DocumentDecorations.scss';
-import { DocumentView } from "../nodes/DocumentView";
-import { DefaultStyleProvider } from "../StyleProvider";
-import "./CollectionSchemaView.scss";
-import { CollectionSubView } from "./CollectionSubView";
-// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
-
-export enum ColumnType {
- Any,
- Number,
- String,
- Boolean,
- Doc,
- Image,
- List,
- Date
-}
-// this map should be used for keys that should have a const type of value
-const columnTypes: Map<string, ColumnType> = new Map([
- ["title", ColumnType.String],
- ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number],
- ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean],
- ["_curPage", ColumnType.Number], ["_currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number]
-]);
-
-@observer
-export class CollectionSchemaView extends CollectionSubView(doc => doc) {
- private _previewCont?: HTMLDivElement;
-
- @observable _previewDoc: Doc | undefined = undefined;
- @observable _focusedTable: Doc = this.props.Document;
- @observable _col: any = "";
- @observable _menuWidth = 0;
- @observable _headerOpen = false;
- @observable _headerIsEditing = false;
- @observable _menuHeight = 0;
- @observable _pointerX = 0;
- @observable _pointerY = 0;
- @observable _openTypes: boolean = false;
-
- @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); }
- @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; }
- @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth(); }
- @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
- @computed get scale() { return this.props.ScreenToLocalTransform().Scale; }
- @computed get columns() { return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); }
- set columns(columns: SchemaHeaderField[]) { this.props.Document._schemaHeaders = new List<SchemaHeaderField>(columns); }
-
- @computed get menuCoordinates() {
- let searchx = 0;
- let searchy = 0;
- if (this.props.Document._searchDoc) {
- const el = document.getElementsByClassName("collectionSchemaView-searchContainer")[0];
- if (el !== undefined) {
- const rect = el.getBoundingClientRect();
- searchx = rect.x;
- searchy = rect.y;
- }
- }
- const x = Math.max(0, Math.min(document.body.clientWidth - this._menuWidth, this._pointerX)) - searchx;
- const y = Math.max(0, Math.min(document.body.clientHeight - this._menuHeight, this._pointerY)) - searchy;
- return this.props.ScreenToLocalTransform().transformPoint(x, y);
- }
-
- get documentKeys() {
- const docs = this.childDocs;
- const keys: { [key: string]: boolean } = {};
- // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields.
- // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be
- // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked.
- // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu
- // is displayed (unlikely) it won't show up until something else changes.
- //TODO Types
- untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false))));
-
- this.columns.forEach(key => keys[key.heading] = true);
- return Array.from(Object.keys(keys));
- }
-
- @action setHeaderIsEditing = (isEditing: boolean) => this._headerIsEditing = isEditing;
-
- @undoBatch
- setColumnType = action((columnField: SchemaHeaderField, type: ColumnType): void => {
- this._openTypes = false;
- if (columnTypes.get(columnField.heading)) return;
-
- const columns = this.columns;
- const index = columns.indexOf(columnField);
- if (index > -1) {
- columnField.setType(NumCast(type));
- columns[index] = columnField;
- this.columns = columns;
- }
- });
-
- @undoBatch
- setColumnColor = (columnField: SchemaHeaderField, color: string): void => {
- const columns = this.columns;
- const index = columns.indexOf(columnField);
- if (index > -1) {
- columnField.setColor(color);
- columns[index] = columnField;
- this.columns = columns; // need to set the columns to trigger rerender
- }
- }
-
- @undoBatch
- @action
- setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => {
- const columns = this.columns;
- columns.forEach(col => col.setDesc(undefined));
-
- const index = columns.findIndex(c => c.heading === columnField.heading);
- const column = columns[index];
- column.setDesc(descending);
- columns[index] = column;
- this.columns = columns;
- }
-
- renderTypes = (col: any) => {
- if (columnTypes.get(col.heading)) return (null);
-
- const type = col.type;
-
- const anyType = <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Any)}>
- <FontAwesomeIcon icon={"align-justify"} size="sm" />
- Any
- </div>;
-
- const numType = <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Number)}>
- <FontAwesomeIcon icon={"hashtag"} size="sm" />
- Number
- </div>;
-
- const textType = <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.String)}>
- <FontAwesomeIcon icon={"font"} size="sm" />
- Text
- </div>;
-
- const boolType = <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Boolean)}>
- <FontAwesomeIcon icon={"check-square"} size="sm" />
- Checkbox
- </div>;
-
- const listType = <div className={"columnMenu-option" + (type === ColumnType.List ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.List)}>
- <FontAwesomeIcon icon={"list-ul"} size="sm" />
- List
- </div>;
-
- const docType = <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Doc)}>
- <FontAwesomeIcon icon={"file"} size="sm" />
- Document
- </div>;
-
- const imageType = <div className={"columnMenu-option" + (type === ColumnType.Image ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Image)}>
- <FontAwesomeIcon icon={"image"} size="sm" />
- Image
- </div>;
-
- const dateType = <div className={"columnMenu-option" + (type === ColumnType.Date ? " active" : "")} onClick={() => this.setColumnType(col, ColumnType.Date)}>
- <FontAwesomeIcon icon={"calendar"} size="sm" />
- Date
- </div>;
-
-
- const allColumnTypes = <div className="columnMenu-types">
- {anyType}
- {numType}
- {textType}
- {boolType}
- {listType}
- {docType}
- {imageType}
- {dateType}
- </div>;
-
- const justColType = type === ColumnType.Any ? anyType : type === ColumnType.Number ? numType :
- type === ColumnType.String ? textType : type === ColumnType.Boolean ? boolType :
- type === ColumnType.List ? listType : type === ColumnType.Doc ? docType :
- type === ColumnType.Date ? dateType : imageType;
-
- return (
- <div className="collectionSchema-headerMenu-group" onClick={action(() => this._openTypes = !this._openTypes)}>
- <div>
- <label style={{ cursor: "pointer" }}>Column type:</label>
- <FontAwesomeIcon icon={"caret-down"} size="lg" style={{ float: "right", transform: `rotate(${this._openTypes ? "180deg" : 0})`, transition: "0.2s all ease" }} />
- </div>
- {this._openTypes ? allColumnTypes : justColType}
- </div >
- );
- }
-
- renderSorting = (col: any) => {
- const sort = col.desc;
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Sort by:</label>
- <div className="columnMenu-sort">
- <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.setColumnSort(col, true)}>
- <FontAwesomeIcon icon="sort-amount-down" size="sm" />
- Sort descending
- </div>
- <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.setColumnSort(col, false)}>
- <FontAwesomeIcon icon="sort-amount-up" size="sm" />
- Sort ascending
- </div>
- <div className="columnMenu-option" onClick={() => this.setColumnSort(col, undefined)}>
- <FontAwesomeIcon icon="times" size="sm" />
- Clear sorting
- </div>
- </div>
- </div>
- );
- }
-
- renderColors = (col: any) => {
- const selected = col.color;
-
- const pink = PastelSchemaPalette.get("pink2");
- const purple = PastelSchemaPalette.get("purple2");
- const blue = PastelSchemaPalette.get("bluegreen1");
- const yellow = PastelSchemaPalette.get("yellow4");
- const red = PastelSchemaPalette.get("red2");
- const gray = "#f1efeb";
-
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Color:</label>
- <div className="columnMenu-colors">
- <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.setColumnColor(col, pink!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.setColumnColor(col, purple!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.setColumnColor(col, blue!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.setColumnColor(col, yellow!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.setColumnColor(col, red!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.setColumnColor(col, gray)}></div>
- </div>
- </div>
- );
- }
-
- @undoBatch
- @action
- changeColumns = (oldKey: string, newKey: string, addNew: boolean, filter?: string) => {
- const columns = this.columns;
- if (columns === undefined) {
- this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, "f1efeb")]);
- } else {
- if (addNew) {
- columns.push(new SchemaHeaderField(newKey, "f1efeb"));
- this.columns = columns;
- } else {
- const index = columns.map(c => c.heading).indexOf(oldKey);
- if (index > -1) {
- const column = columns[index];
- column.setHeading(newKey);
- columns[index] = column;
- this.columns = columns;
- if (filter) {
- Doc.setDocFilter(this.props.Document, newKey, filter, "match");
- }
- else {
- this.props.Document._docFilters = undefined;
- }
- }
- }
- }
- }
-
- @action
- openHeader = (col: any, screenx: number, screeny: number) => {
- this._col = col;
- this._headerOpen = true;
- this._pointerX = screenx;
- this._pointerY = screeny;
- }
-
- @action
- closeHeader = () => { this._headerOpen = false; }
-
- @undoBatch
- @action
- deleteColumn = (key: string) => {
- const columns = this.columns;
- if (columns === undefined) {
- this.columns = new List<SchemaHeaderField>([]);
- } else {
- const index = columns.map(c => c.heading).indexOf(key);
- if (index > -1) {
- columns.splice(index, 1);
- this.columns = columns;
- }
- }
- this.closeHeader();
- }
-
- getPreviewTransform = (): Transform => {
- return this.props.ScreenToLocalTransform().translate(- this.borderWidth - NumCast(COLLECTION_BORDER_WIDTH) - this.tableWidth, - this.borderWidth);
- }
-
- @action
- onHeaderClick = (e: React.PointerEvent) => {
- e.stopPropagation();
- }
-
- @action
- onWheel(e: React.WheelEvent) {
- const scale = this.props.ScreenToLocalTransform().Scale;
- this.props.isContentActive(true) && e.stopPropagation();
- }
-
- @computed get renderMenuContent() {
- TraceMobx();
- return <div className="collectionSchema-header-menuOptions">
- {this.renderTypes(this._col)}
- {this.renderColors(this._col)}
- <div className="collectionSchema-headerMenu-group">
- <button onClick={() => { this.deleteColumn(this._col.heading); }}
- >Hide Column</button>
- </div>
- </div>;
- }
-
- private createTarget = (ele: HTMLDivElement) => {
- this._previewCont = ele;
- super.CreateDropTarget(ele);
- }
-
- isFocused = (doc: Doc, outsideReaction: boolean): boolean => this.props.isSelected(outsideReaction) && doc === this._focusedTable;
-
- @action setFocused = (doc: Doc) => this._focusedTable = doc;
-
- @action setPreviewDoc = (doc: Opt<Doc>) => {
- SelectionManager.SelectSchemaView(this, doc);
- this._previewDoc = doc;
- }
-
- //toggles preview side-panel of schema
- @action
- toggleExpander = () => {
- this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0;
- }
-
- onDividerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, this.toggleExpander);
- }
- @action
- onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => {
- const nativeWidth = this._previewCont!.getBoundingClientRect();
- const minWidth = 40;
- const maxWidth = 1000;
- const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0];
- const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth;
- this.props.Document.schemaPreviewWidth = width;
- return false;
- }
-
- onPointerDown = (e: React.PointerEvent): void => {
- if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
- if (this.props.isSelected(true)) e.stopPropagation();
- else this.props.select(false);
- }
- }
-
- @computed
- get previewDocument(): Doc | undefined { return this._previewDoc; }
-
- @computed
- get dividerDragger() {
- return this.previewWidth() === 0 ? (null) :
- <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} >
- <div className="collectionSchemaView-dividerDragger" />
- </div>;
- }
-
- @computed
- get previewPanel() {
- return <div ref={this.createTarget} style={{ width: `${this.previewWidth()}px` }}>
- {!this.previewDocument ? (null) :
- <DocumentView
- Document={this.previewDocument}
- DataDoc={undefined}
- fitContentsToDoc={returnTrue}
- freezeDimensions={true}
- dontCenter={"y"}
- focus={DocUtils.DefaultFocus}
- renderDepth={this.props.renderDepth}
- rootSelected={this.rootSelected}
- PanelWidth={this.previewWidth}
- PanelHeight={this.previewHeight}
- isContentActive={returnTrue}
- isDocumentActive={returnFalse}
- ScreenToLocalTransform={this.getPreviewTransform}
- docFilters={this.childDocFilters}
- docRangeFilters={this.childDocRangeFilters}
- searchFilterDocs={this.searchFilterDocs}
- styleProvider={DefaultStyleProvider}
- layerProvider={undefined}
- docViewPath={returnEmptyDoclist}
- ContainingCollectionDoc={this.props.CollectionView?.props.Document}
- ContainingCollectionView={this.props.CollectionView}
- moveDocument={this.props.moveDocument}
- addDocument={this.props.addDocument}
- removeDocument={this.props.removeDocument}
- whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- bringToFront={returnFalse}
- />}
- </div>;
- }
-
- @computed
- get schemaTable() {
- return <SchemaTable
- Document={this.props.Document}
- PanelHeight={this.props.PanelHeight}
- PanelWidth={this.props.PanelWidth}
- childDocs={this.childDocs}
- CollectionView={this.props.CollectionView}
- ContainingCollectionView={this.props.ContainingCollectionView}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- fieldKey={this.props.fieldKey}
- renderDepth={this.props.renderDepth}
- moveDocument={this.props.moveDocument}
- ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- active={this.props.isContentActive}
- onDrop={this.onExternalDrop}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- isSelected={this.props.isSelected}
- isFocused={this.isFocused}
- setFocused={this.setFocused}
- setPreviewDoc={this.setPreviewDoc}
- deleteDocument={this.props.removeDocument}
- addDocument={this.props.addDocument}
- dataDoc={this.props.DataDoc}
- columns={this.columns}
- documentKeys={this.documentKeys}
- headerIsEditing={this._headerIsEditing}
- openHeader={this.openHeader}
- onClick={this.onTableClick}
- onPointerDown={emptyFunction}
- onResizedChange={this.onResizedChange}
- setColumns={this.setColumns}
- reorderColumns={this.reorderColumns}
- changeColumns={this.changeColumns}
- setHeaderIsEditing={this.setHeaderIsEditing}
- changeColumnSort={this.setColumnSort}
- />;
- }
-
- @computed
- public get schemaToolbar() {
- return <div className="collectionSchemaView-toolbar">
- <div className="collectionSchemaView-toolbar-item">
- <div id="preview-schema-checkbox-div">
- <input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} />
- Show Preview
- </div>
- </div>
- </div>;
- }
-
- onSpecificMenu = (e: React.MouseEvent) => {
- if ((e.target as any)?.className?.includes?.("collectionSchemaView-cell") || (e.target instanceof HTMLSpanElement)) {
- const cm = ContextMenu.Instance;
- const options = cm.findByDescription("Options...");
- const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
- optionItems.push({ description: "remove", event: () => this._previewDoc && this.props.removeDocument?.(this._previewDoc), icon: "trash" });
- !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" });
- cm.displayMenu(e.clientX, e.clientY);
- (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this.
- e.stopPropagation();
- }
- }
-
- @action
- onTableClick = (e: React.MouseEvent): void => {
- if (!(e.target as any)?.className?.includes?.("collectionSchemaView-cell") && !(e.target instanceof HTMLSpanElement)) {
- this.setPreviewDoc(undefined);
- } else {
- e.stopPropagation();
- }
- this.setFocused(this.props.Document);
- this.closeHeader();
- }
-
- onResizedChange = (newResized: Resize[], event: any) => {
- const columns = this.columns;
- newResized.forEach(resized => {
- const index = columns.findIndex(c => c.heading === resized.id);
- const column = columns[index];
- column.setWidth(resized.value);
- columns[index] = column;
- });
- this.columns = columns;
- }
-
- @action
- setColumns = (columns: SchemaHeaderField[]) => this.columns = columns
-
- @undoBatch
- reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => {
- const columns = [...columnsValues];
- const oldIndex = columns.indexOf(toMove);
- const relIndex = columns.indexOf(relativeTo);
- const newIndex = (oldIndex > relIndex && !before) ? relIndex + 1 : (oldIndex < relIndex && before) ? relIndex - 1 : relIndex;
-
- if (oldIndex === newIndex) return;
-
- columns.splice(newIndex, 0, columns.splice(oldIndex, 1)[0]);
- this.columns = columns;
- }
-
- onZoomMenu = (e: React.WheelEvent) => this.props.isContentActive(true) && e.stopPropagation();
-
- render() {
- TraceMobx();
- if (!this.props.isContentActive()) setTimeout(() => this.closeHeader(), 0);
- const menuContent = this.renderMenuContent;
- const menu = <div className="collectionSchema-header-menu"
- onWheel={e => this.onZoomMenu(e)}
- onPointerDown={e => this.onHeaderClick(e)}
- style={{ transform: `translate(${(this.menuCoordinates[0])}px, ${(this.menuCoordinates[1])}px)` }}>
- <Measure offset onResize={action((r: any) => {
- const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height);
- this._menuWidth = dim[0]; this._menuHeight = dim[1];
- })}>
- {({ measureRef }) => <div ref={measureRef}> {menuContent} </div>}
- </Measure>
- </div>;
- return <div className={"collectionSchemaView" + (this.props.Document._searchDoc ? "-searchContainer" : "-container")}
- style={{
- overflow: this.props.scrollOverflow === true ? "scroll" : undefined, backgroundColor: "white",
- pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined,
- width: this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative",
- }} >
- <div className="collectionSchemaView-tableContainer"
- style={{ width: `calc(100% - ${this.previewWidth()}px)` }}
- onContextMenu={this.onSpecificMenu}
- onPointerDown={this.onPointerDown}
- onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}
- onDrop={e => this.onExternalDrop(e, {})}
- ref={this.createTarget}>
- {this.schemaTable}
- </div>
- {this.dividerDragger}
- {!this.previewWidth() ? (null) : this.previewPanel}
- {this._headerOpen && this.props.isContentActive() ? menu : null}
- </div>;
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 65022fdfd..89da6692a 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -372,10 +372,11 @@ export class CollectionStackedTimeline extends CollectionSubView<
startTag: string,
endTag: string,
anchorStartTime?: number,
- anchorEndTime?: number
+ anchorEndTime?: number,
+ docAnchor?: Doc
) {
if (anchorStartTime === undefined) return rootDoc;
- const anchor = Docs.Create.LabelDocument({
+ const anchor = docAnchor ?? Docs.Create.LabelDocument({
title: ComputedField.MakeFunction(
`"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`
) as any,
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index cb8b55cb2..5dffc65fc 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -356,7 +356,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const batch = UndoManager.StartBatch("youtube upload");
const generatedDocuments: Doc[] = [];
- this.slowLoadDocuments((uriList || text).split("v=")[1], options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end);
+ this.slowLoadDocuments((uriList || text).split("v=")[1].split("&")[0], options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end);
return;
}
@@ -378,7 +378,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
// }
}
if (uriList) {
- console.log("Web URI = ", uriList);
// const existingWebDoc = await Hypothesis.findWebDoc(uriList);
// if (existingWebDoc) {
// const alias = Doc.MakeAlias(existingWebDoc);
@@ -390,7 +389,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
// addDocument(alias);
// } else
{
- console.log("Adding ...");
const newDoc = Docs.Create.WebDocument(uriList.split("#annotations:")[0], {// clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig)
...options,
title: uriList.split("#annotations:")[0],
@@ -399,8 +397,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
_nativeWidth: 850,
useCors: true
});
- console.log(" ... " + newDoc.title);
- console.log(" ... " + addDocument(newDoc) + " " + newDoc.title);
+ addDocument(newDoc);
}
return;
}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 0dd1e6e36..a7ca57b0b 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -246,12 +246,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null);
@computed get childLayoutString() { return StrCast(this.rootDoc.childLayoutString); }
- /**
- * Shows the filter icon if it's a user-created collection which isn't a dashboard and has some docFilters applied on it or on the current dashboard.
- */
- @computed get showFilterIcon() {
- return this.props.Document.viewType !== CollectionViewType.Docking && !Doc.IsSystem(this.props.Document) && this._subView?.IsFiltered();
- }
@observable _subView: any = undefined;
@@ -280,12 +274,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
style={{ pointerEvents: this.props.layerProvider?.(this.rootDoc) === false ? "none" : undefined }}>
{this.showIsTagged()}
{this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
- {this.showFilterIcon ?
- <FontAwesomeIcon icon={"filter"} size="lg"
- style={{ position: 'absolute', top: '1%', right: '1%', cursor: "pointer", padding: 1, color: this.showFilterIcon === "hasFilter" ? '#18c718bd' : "orange", zIndex: 1 }}
- onPointerDown={action(e => { this.props.select(false); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })}
- />
- : (null)}
</div>);
}
}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 73c065482..6c6a2fb05 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -22,6 +22,7 @@ import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { Colors, Shadows } from '../global/globalEnums';
import { LightboxView } from '../LightboxView';
import { DocFocusOptions, DocumentView, DocumentViewProps } from "../nodes/DocumentView";
import { PinProps, PresBox, PresMovement } from '../nodes/trails';
@@ -32,8 +33,6 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
import { CollectionView, CollectionViewType } from './CollectionView';
import "./TabDocView.scss";
import React = require("react");
-import Color = require('color');
-import { Colors, Shadows } from '../global/globalEnums';
const _global = (window /* browser */ || global /* node */) as any;
interface TabDocViewProps {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 97de097e0..7f2128230 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -481,13 +481,16 @@ export class TreeView extends React.Component<TreeViewProps> {
}
@computed get validExpandViewTypes() {
- if (this.doc.viewType === CollectionViewType.Docking) return [this.fieldKey];
+ if (this.props.treeView.dashboardMode && Doc.UserDoc().noviceMode) {
+ return [this.doc.viewType === CollectionViewType.Docking ? this.fieldKey : "layout"];
+ }
const annos = () => DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : "";
const links = () => DocListCast(this.doc.links).length ? "links" : "";
- const data = () => this.childDocs && !this.props.treeView.dashboardMode ? this.fieldKey : "";
+ const data = () => this.childDocs ? this.fieldKey : "";
const aliases = () => this.props.treeView.dashboardMode ? "" : "aliases";
const fields = () => Doc.UserDoc().noviceMode ? "" : "fields";
- return [data(), "layout", ...(this.props.treeView.fileSysMode ? [aliases(), links(), annos()] : []), fields()].filter(m => m);
+ const layout = this.doc.viewType === CollectionViewType.Docking ? [] : ["layout"];
+ return [data(), ...layout, ...(this.props.treeView.fileSysMode ? [aliases(), links(), annos()] : []), fields()].filter(m => m);
}
@action
expandNextviewType = () => {
@@ -817,7 +820,7 @@ export class TreeView extends React.Component<TreeViewProps> {
childDocs: Doc[],
treeView: CollectionTreeView,
parentTreeView: CollectionTreeView | TreeView | undefined,
- conainerCollection: Doc,
+ containerCollection: Doc,
dataDoc: Doc | undefined,
parentCollectionDoc: Doc | undefined,
containerPrevSibling: Doc | undefined,
@@ -843,16 +846,16 @@ export class TreeView extends React.Component<TreeViewProps> {
unobserveHeight: (ref: any) => void,
contextMenuItems: ({ script: ScriptField, filter: ScriptField, label: string, icon: string }[])
) {
- const viewSpecScript = Cast(conainerCollection.viewSpecScript, ScriptField);
+ const viewSpecScript = Cast(containerCollection.viewSpecScript, ScriptField);
if (viewSpecScript) {
childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result);
}
- const docs = TreeView.sortDocs(childDocs, StrCast(conainerCollection.treeViewSortCriterion));
+ const docs = TreeView.sortDocs(childDocs, StrCast(containerCollection.treeViewSortCriterion));
const rowWidth = () => panelWidth() - treeBulletWidth();
const treeViewRefs = new Map<Doc, TreeView | undefined>();
return docs.filter(child => child instanceof Doc).map((child, i) => {
- const pair = Doc.GetLayoutDataDocPair(conainerCollection, dataDoc, child);
+ const pair = Doc.GetLayoutDataDocPair(containerCollection, dataDoc, child);
if (!pair.layout || pair.data instanceof Promise) {
return (null);
}
@@ -880,7 +883,7 @@ export class TreeView extends React.Component<TreeViewProps> {
return <TreeView key={child[Id]} ref={r => treeViewRefs.set(child, r ? r : undefined)}
document={pair.layout}
dataDoc={pair.data}
- containerCollection={conainerCollection}
+ containerCollection={containerCollection}
prevSibling={docs[i]}
treeView={treeView}
indentDocument={indent}
@@ -888,7 +891,7 @@ export class TreeView extends React.Component<TreeViewProps> {
onCheckedClick={onCheckedClick}
onChildClick={onChildClick}
renderDepth={renderDepth}
- removeDoc={StrCast(conainerCollection.freezeChildren).includes("remove") ? undefined : remove}
+ removeDoc={StrCast(containerCollection.freezeChildren).includes("remove") ? undefined : remove}
addDocument={addDocument}
styleProvider={styleProvider}
panelWidth={rowWidth}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 37444a9dc..9fed82dae 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -7,6 +7,7 @@ import { Cast, NumCast, StrCast } from "../../../../fields/Types";
import { aggregateBounds } from "../../../../Utils";
import { CurrentUserUtils } from "../../../util/CurrentUserUtils";
import React = require("react");
+import { ColorScheme } from "../../../util/SettingsManager";
export interface ViewDefBounds {
type: string;
@@ -361,7 +362,7 @@ export function computeTimelineLayout(
groupNames.push({ type: "text", text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined });
}
- const divider = { type: "div", color: CurrentUserUtils.ActiveDashboard?.darkScheme ? "dimGray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined };
+ const divider = { type: "div", color: CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "dimgray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined };
return normalizeResults(panelDim, fontHeight, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider]);
function layoutDocsAtTime(keyDocs: Doc[], key: number) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 3b3e069d8..bb4cae8c6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,11 +1,13 @@
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../../../fields/Doc";
+import { Doc, Field } from "../../../../fields/Doc";
import { Id } from "../../../../fields/FieldSymbols";
import { List } from "../../../../fields/List";
-import { NumCast, StrCast } from "../../../../fields/Types";
+import { NumCast } from "../../../../fields/Types";
import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { CurrentUserUtils } from "../../../util/CurrentUserUtils";
import { LinkManager } from "../../../util/LinkManager";
+import { ColorScheme } from "../../../util/SettingsManager";
import { SnappingManager } from "../../../util/SnappingManager";
import { DocumentView } from "../../nodes/DocumentView";
import "./CollectionFreeFormLinkView.scss";
@@ -40,7 +42,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const { A, B, LinkDocs } = this.props;
const linkDoc = LinkDocs[0];
if (SnappingManager.GetIsDragging() || !A.ContentDiv || !B.ContentDiv) return;
- setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render()
+ setTimeout(action(() => this._opacity = 0.75), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render()
setTimeout(action(() => (!LinkDocs.length || !linkDoc.linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line.
const acont = A.ContentDiv.getElementsByClassName("linkAnchorBox-cont");
const bcont = B.ContentDiv.getElementsByClassName("linkAnchorBox-cont");
@@ -178,18 +180,29 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
render() {
if (!this.renderData) return (null);
+
const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData;
LinkManager.currentLink = this.props.LinkDocs[0];
- const linkRelationship = StrCast(LinkManager.currentLink?.linkRelationship); //get string representing relationship
+ const linkRelationship = Field.toString(LinkManager.currentLink?.linkRelationship as any as Field); //get string representing relationship
const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List<string>;
const linkColorList = Doc.UserDoc().linkColorList as List<string>;
+ const linkRelationshipSizes = Doc.UserDoc().linkRelationshipSizes as List<number>;
+ const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship);
+
+ const linkSize = currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex];
+
//access stroke color using index of the relationship in the color list (default black)
- const strokeColor = linkRelationshipList.indexOf(linkRelationship) === -1 ? "black" : linkColorList[linkRelationshipList.indexOf(linkRelationship)];
+ const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? "black" : linkColorList[currRelationshipIndex];
+
+ //calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has)
+ //thickness varies linearly from 3px to 12px for increasing link count
+ const strokeWidth = linkSize === -1 ? "3px" : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + "px";
+
return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<>
- <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2", stroke: strokeColor }}
+ <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, /*strokeDasharray: "2 2",*/ stroke, strokeWidth }}
d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} />
{textX === undefined ? (null) : <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} >
- {StrCast(this.props.LinkDocs[0].description)}
+ {Field.toString(this.props.LinkDocs[0].description as any as Field)}
</text>}
</>);
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index be0b078ec..febccbfcc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, ObservableMap } from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
import { DateField } from "../../../../fields/DateField";
@@ -50,7 +50,7 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso
import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
-import Color = require("color");
+import { ColorScheme } from "../../../util/SettingsManager";
export const panZoomSchema = createSchema({
_panX: "number",
@@ -75,7 +75,7 @@ export type collectionFreeformViewProps = {
scaleField?: string;
noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale)
engineProps?: any;
- dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are trnasparent or not.
+ dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are transparent or not.
// However, this screws up interactions since only the top layer gets events. so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents.
};
@@ -811,14 +811,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) {
if (!this.isAnnotationOverlay && clamp) {
// this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
- const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout);
- const measuredDocs = docs.filter(doc => doc && this.childDataProvider(doc, "") && this.childSizeProvider(doc, "")).
- map(doc => ({ ...this.childDataProvider(doc, ""), ...this.childSizeProvider(doc, "") }));
+ const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc);
+ const measuredDocs = docs.map(doc => ({ pos: this.childPositionProviderUnmemoized(doc, ""), size: this.childSizeProviderUnmemoized(doc, "") }))
+ .filter(({ pos, size }) => pos && size).map(({ pos, size }) => ({ pos: pos!, size: size! }));
if (measuredDocs.length) {
- const ranges = measuredDocs.reduce(({ xrange, yrange }, { x, y, width, height }) => // computes range of content
+ const ranges = measuredDocs.reduce(({ xrange, yrange }, { pos, size }) => // computes range of content
({
- xrange: { min: Math.min(xrange.min, x), max: Math.max(xrange.max, x + width) },
- yrange: { min: Math.min(yrange.min, y), max: Math.max(yrange.max, y + height) }
+ xrange: { min: Math.min(xrange.min, pos.x), max: Math.max(xrange.max, pos.x + (size.width || 0)) },
+ yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) }
})
, {
xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE },
@@ -902,7 +902,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
HistoryUtil.pushState(state);
}
}
- SelectionManager.DeselectAll();
+ if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
+ SelectionManager.DeselectAll();
+ }
if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined) {
this.props.focus(doc, options);
} else {
@@ -1034,7 +1036,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
showTitle={this.props.childShowTitle}
dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" :
- (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : undefined}
+ (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : this.props.pointerEvents}
jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
//fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
/>;
@@ -1103,10 +1105,16 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
}
+ childPositionProviderUnmemoized = (doc: Doc, replica: string) => {
+ return this._layoutPoolData.get(doc[Id] + (replica || ""));
+ }
childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc, replica: string) {
return this._layoutPoolData.get(doc[Id] + (replica || ""));
}.bind(this));
+ childSizeProviderUnmemoized = (doc: Doc, replica: string) => {
+ return this._layoutSizeData.get(doc[Id] + (replica || ""));
+ }
childSizeProvider = computedFn(function childSizeProvider(this: any, doc: Doc, replica: string) {
return this._layoutSizeData.get(doc[Id] + (replica || ""));
}.bind(this));
@@ -1417,6 +1425,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
const renderGridSpace = gridSpace * this.zoomScaling();
const w = this.props.PanelWidth() + 2 * renderGridSpace;
const h = this.props.PanelHeight() + 2 * renderGridSpace;
+ const strokeStyle = CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "rgba(255,255,255,0.5)" : "rgba(0, 0,0,0.5)";
return <canvas className="collectionFreeFormView-grid" width={w} height={h} style={{ transform: `translate(${shiftX}px, ${shiftY}px)` }}
ref={(el) => {
const ctx = el?.getContext('2d');
@@ -1427,7 +1436,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]);
ctx.clearRect(0, 0, w, h);
if (ctx) {
- ctx.strokeStyle = "rgba(0, 0, 0, 0.5)";
+ ctx.strokeStyle = strokeStyle;
ctx.beginPath();
for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) {
ctx.moveTo(x, Cy - h);
@@ -1463,7 +1472,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
getContainerTransform={this.getContainerTransform}
getTransform={this.getTransform}
isAnnotationOverlay={this.isAnnotationOverlay}>
- <div ref={this._marqueeRef} style={{ display: this.props.dontRenderDocuments ? "none" : undefined }}>
+ <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0 : undefined }}>
{this.layoutDoc._backgroundGridShow ? this.backgroundGrid : (null)}
<CollectionFreeFormViewPannableContents
isAnnotationOverlay={this.isAnnotationOverlay}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index cedeb1112..1f59f9732 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -3,20 +3,22 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from "@material-ui/core";
import { observer } from "mobx-react";
import { unimplementedFunction } from "../../../../Utils";
+import { DocumentType } from "../../../documents/DocumentTypes";
+import { SelectionManager } from "../../../util/SelectionManager";
import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu";
@observer
export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
static Instance: MarqueeOptionsMenu;
- public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+ public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => void = unimplementedFunction;
public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public showMarquee: () => void = unimplementedFunction;
public hideMarquee: () => void = unimplementedFunction;
public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
-
+ public isShown = () => this._opacity > 0;
constructor(props: Readonly<{}>) {
super(props);
@@ -26,42 +28,52 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
render() {
const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 19, transform: 'translate(-2px, -2px)' }} />;
const buttons = [
- <Tooltip key="group" title={<><div className="dash-tooltip">Create a Collection</div></>} placement="bottom">
+ <Tooltip key="collect" title={<div className="dash-tooltip">Create a Collection</div>} placement="bottom">
<button
className="antimodeMenu-button"
onPointerDown={this.createCollection}>
<FontAwesomeIcon icon="object-group" size="lg" />
</button>
</Tooltip>,
- <Tooltip key="summarize" title={<><div className="dash-tooltip">Summarize Documents</div></>} placement="bottom">
+ <Tooltip key="group" title={<div className="dash-tooltip">Create a Grouping</div>} placement="bottom">
<button
className="antimodeMenu-button"
- onPointerDown={this.summarize}>
- <FontAwesomeIcon icon="compress-arrows-alt" size="lg" />
+ onPointerDown={e => this.createCollection(e, true)}>
+ <FontAwesomeIcon icon="layer-group" size="lg" />
</button>
</Tooltip>,
- <Tooltip key="delete" title={<><div className="dash-tooltip">Delete Documents</div></>} placement="bottom">
+ <Tooltip key="summarize" title={<div className="dash-tooltip">Summarize Documents</div>} placement="bottom">
<button
className="antimodeMenu-button"
- onPointerDown={this.delete}>
- <FontAwesomeIcon icon="trash-alt" size="lg" />
+ onPointerDown={this.summarize}>
+ <FontAwesomeIcon icon="compress-arrows-alt" size="lg" />
</button>
</Tooltip>,
- <Tooltip key="inkToText" title={<><div className="dash-tooltip">Change to Text</div></>} placement="bottom">
+ <Tooltip key="delete" title={<div className="dash-tooltip">Delete Documents</div>} placement="bottom">
<button
className="antimodeMenu-button"
- onPointerDown={this.inkToText}>
- <FontAwesomeIcon icon="font" size="lg" />
+ onPointerDown={this.delete}>
+ <FontAwesomeIcon icon="trash-alt" size="lg" />
</button>
</Tooltip>,
- <Tooltip key="pinWithView" title={<><div className="dash-tooltip">Pin with selected region</div></>} placement="bottom">
+ <Tooltip key="pinWithView" title={<div className="dash-tooltip">Pin with selected region</div>} placement="bottom">
<button
className="antimodeMenu-button"
onPointerDown={this.pinWithView}>
- <>{presPinWithViewIcon}</>
+ {presPinWithViewIcon}
</button>
</Tooltip>,
];
+ if (false && !SelectionManager.Views().some(v => v.props.Document.type !== DocumentType.INK)) {
+ buttons.push(
+ <Tooltip key="inkToText" title={<div className="dash-tooltip">Change to Text</div>} placement="bottom">
+ <button
+ className="antimodeMenu-button"
+ onPointerDown={this.inkToText}>
+ <FontAwesomeIcon icon="font" size="lg" />
+ </button>
+ </Tooltip>);
+ }
return this.getElement(buttons);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 81f6307d1..24a7d77e0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -646,8 +646,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
render() {
return <div className="marqueeView"
style={{
- overflow: //(!this.props.ContainingCollectionView && this.props.isAnnotationOverlay) ? "visible" :
- StrCast(this.props.Document._overflow),
+ overflow: StrCast(this.props.Document._overflow),
cursor: "hand"
}}
onDragOver={e => e.preventDefault()}
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 7fe95fef0..18a715edf 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -109,7 +109,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
}
myContextMenu = (e: React.MouseEvent) => {
- console.log("STOPPING");
e.stopPropagation();
e.preventDefault();
}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
index ed196349e..a439a7998 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
@@ -38,27 +38,35 @@ export interface CellProps {
row: number;
col: number;
rowProps: CellInfo;
+ // currently unused
CollectionView: Opt<CollectionView>;
+ // currently unused
ContainingCollection: Opt<CollectionView>;
Document: Doc;
+ // column name
fieldKey: string;
+ // currently unused
renderDepth: number;
+ // called when a button is pressed on the node itself
addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined,
addDocument: (document: Doc | Doc[]) => boolean) => boolean;
isFocused: boolean;
changeFocusedCellByIndex: (row: number, col: number) => void;
+ // set whether the cell is in the isEditing mode
setIsEditing: (isEditing: boolean) => void;
isEditable: boolean;
setPreviewDoc: (doc: Doc) => void;
setComputed: (script: string, doc: Doc, field: string, row: number, col: number) => boolean;
getField: (row: number, col?: number) => void;
+ // currnetly unused
showDoc: (doc: Doc | undefined, dataDoc?: any, screenX?: number, screenY?: number) => void;
}
@observer
export class CollectionSchemaCell extends React.Component<CellProps> {
+ // return a field key that is corrected for whether it COMMENT
public static resolvedFieldKey(column: string, rowDoc: Doc) {
const fieldKey = column;
if (fieldKey.startsWith("*")) {
@@ -72,7 +80,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
@observable protected _isEditing: boolean = false;
protected _focusRef = React.createRef<HTMLDivElement>();
protected _rowDoc = this.props.rowProps.original;
+ // Gets the serialized data in proto form of the base proto that this document's proto inherits from
protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original);
+ // methods for dragging and dropping
protected _dropDisposer?: DragManager.DragDropDisposer;
@observable contents: string = "";
@@ -81,6 +91,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
@action
onKeyDown = (e: KeyboardEvent): void => {
+ // If a cell is editable and clicked, hitting enter shoudl allow the user to edit it
if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) {
document.removeEventListener("keydown", this.onKeyDown);
this._isEditing = true;
@@ -90,7 +101,11 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
@action
isEditingCallback = (isEditing: boolean): void => {
+ // a general method that takes a boolean that determines whether the cell should be in
+ // is-editing mode
+ // remove the event listener if it's there
document.removeEventListener("keydown", this.onKeyDown);
+ // it's not already in is-editing mode, re-add the event listener
isEditing && document.addEventListener("keydown", this.onKeyDown);
this._isEditing = isEditing;
this.props.setIsEditing(isEditing);
@@ -99,13 +114,15 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
@action
onPointerDown = async (e: React.PointerEvent): Promise<void> => {
+ // pan to the cell
this.onItemDown(e);
+ // focus on it
this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
this.props.setPreviewDoc(this.props.rowProps.original);
- console.log("click cell");
let url: string;
if (url = StrCast(this.props.rowProps.row.href)) {
+ // opens up the the doc in a new window, blurring the old one
try {
new URL(url);
const temp = window.open(url)!;
@@ -120,18 +137,25 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
@undoBatch
applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => {
+ // apply a specified change to the cell
const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) });
if (!res.success) return false;
+ // change what is rendered to this new changed cell content
doc[this.renderFieldKey] = res.result;
return true;
+ // return whether the change was successful
}
private drop = (e: Event, de: DragManager.DropEvent) => {
+ // if the drag has data at its completion
if (de.complete.docDragData) {
+ // if only one doc was dragged
if (de.complete.docDragData.draggedDocuments.length === 1) {
+ // update the renderFieldKey
this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0];
}
else {
+ // create schema document reflecting the new column arrangement
const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {});
this._rowDataDoc[this.renderFieldKey] = coll;
}
@@ -140,7 +164,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
}
protected dropRef = (ele: HTMLElement | null) => {
+ // if the drop disposer is not undefined, run its function
this._dropDisposer?.();
+ // if ele is not null, give ele a non-undefined drop disposer
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)));
}
@@ -164,33 +190,46 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>;
}
- @computed get renderFieldKey() { return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original); }
+ @computed get renderFieldKey() {
+ // gets the resolved field key of this cell
+ return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original);
+ }
+
onItemDown = async (e: React.PointerEvent) => {
+ // if the document is a document used to change UI for search results in schema view
if (this.props.Document._searchDoc) {
const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc);
const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null);
+ // Jump to the this document
DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext,
undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc));
}
}
+
renderCellWithType(type: string | undefined) {
const dragRef: React.RefObject<HTMLDivElement> = React.createRef();
+ // the column
const fieldKey = this.renderFieldKey;
+ // the exact cell
const field = this._rowDoc[fieldKey];
const onPointerEnter = (e: React.PointerEvent): void => {
+ // e.buttons === 1 means the left moue pointer is down
if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) {
dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over";
}
};
const onPointerLeave = (e: React.PointerEvent): void => {
+ // change the class name to indicate that the cell is no longer being dragged
dragRef.current!.className = "collectionSchemaView-cellContainer";
};
let contents = Field.toString(field as Field);
+ // display 2 hyphens instead of a blank box for empty cells
contents = contents === "" ? "--" : contents;
+ // classname reflects the tatus of the cell
let className = "collectionSchemaView-cellWrapper";
if (this._isEditing) className += " editing";
if (this.props.isFocused && this.props.isEditable) className += " focused";
@@ -198,19 +237,23 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
const positions = [];
if (StrCast(this.props.Document._searchString).toLowerCase() !== "") {
+ // term is ...promise pending... if the field is a Promise, otherwise it is the cell's contents
let term = (field instanceof Promise) ? "...promise pending..." : contents.toLowerCase();
const search = StrCast(this.props.Document._searchString).toLowerCase();
let start = term.indexOf(search);
let tally = 0;
+ // if search is found in term
if (start !== -1) {
positions.push(start);
}
+ // if search is found in term, continue finding all instances of search in term
while (start < contents?.length && start !== -1) {
term = term.slice(start + search.length + 1);
tally += start + search.length + 1;
start = term.indexOf(search);
positions.push(tally + start);
}
+ // remove the last position
if (positions.length > 1) {
positions.pop();
}
@@ -280,6 +323,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
//two options here: we can strip off outer quotes or we can figure out what's going on with the script
const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
const changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length;
+ // change it if a change is made, otherwise, just compile using the old cell conetnts
script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
// handle numbers and expressions
} else if (inputIsNum || value.startsWith("=")) {
@@ -309,6 +353,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
return retVal;
})}
OnFillDown={async (value: string) => {
+ // computes all of the value preceded by :=
const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
script.compiled && DocListCast(this.props.Document[this.props.fieldKey]).
forEach((doc, i) => value.startsWith(":=") ?
@@ -339,7 +384,10 @@ export class CollectionSchemaStringCell extends CollectionSchemaCell { render()
@observer
export class CollectionSchemaDateCell extends CollectionSchemaCell {
- @computed get _date(): Opt<DateField> { return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined; }
+ @computed get _date(): Opt<DateField> {
+ // if the cell is a date field, cast then contents to a date. Otherrwwise, make the contents undefined.
+ return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined;
+ }
@action
handleChange = (date: any) => {
@@ -378,8 +426,9 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
typecheck: true,
transformer: DocumentIconContainer.getTransformer()
});
-
+ // compile the script
const results = script.compiled && script.run();
+ // if the script was compiled and run
if (results && results.success) {
this._rowDoc[this.renderFieldKey] = results.result;
return true;
@@ -397,6 +446,7 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
@action
isEditingCallback = (isEditing: boolean): void => {
+ // the isEditingCallback from a general CollectionSchemaCell
document.removeEventListener("keydown", this.onKeyDown);
isEditing && document.addEventListener("keydown", this.onKeyDown);
this._isEditing = isEditing;
@@ -405,6 +455,7 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
}
render() {
+ // if there's a doc, render it
return !this._doc ? this.renderCellWithType("document") :
<div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1}
onPointerDown={this.onPointerDown}
@@ -440,11 +491,11 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
export class CollectionSchemaImageCell extends CollectionSchemaCell {
choosePath(url: URL) {
- if (url.protocol === "data") return url.href;
- if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href);
- if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here
+ if (url.protocol === "data") return url.href; // if the url ises the data protocol, just return the href
+ if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
+ if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here — good question
- const ext = path.extname(url.href);
+ const ext = path.extname(url.href); // the extension of the file
return url.href.replace(ext, "_o" + path.extname(url.href));
}
@@ -453,12 +504,13 @@ export class CollectionSchemaImageCell extends CollectionSchemaCell {
const alts = DocListCast(this._rowDoc[this.renderFieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images
const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
+ // If there is a path, follow it; otherwise, follow a link to a default image icon
const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
- const aspect = Doc.NativeAspect(this._rowDoc);
- let width = Math.min(75, this.props.rowProps.width);
- const height = Math.min(75, width / aspect);
- width = height * aspect;
+ const aspect = Doc.NativeAspect(this._rowDoc); // aspect ratio
+ let width = Math.min(75, this.props.rowProps.width); // get a with that is no smaller than 75px
+ const height = Math.min(75, width / aspect); // get a height either proportional to that or 75 px
+ width = height * aspect; // increase the width of the image if necessary to maintain proportionality
const reference = React.createRef<HTMLDivElement>();
return <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
@@ -478,13 +530,13 @@ export class CollectionSchemaListCell extends CollectionSchemaCell {
@computed get _field() { return this._rowDoc[this.renderFieldKey]; }
@computed get _optionsList() { return this._field as List<any>; }
- @observable private _opened = false;
+ @observable private _opened = false; // whether the list is opened
@observable private _text = "select an item";
- @observable private _selectedNum = 0;
+ @observable private _selectedNum = 0; // the index of the list item selected
@action
onSetValue = (value: string) => {
- // change if its a document
+ // change if it's a document
this._optionsList[this._selectedNum] = this._text = value;
(this._field as List<any>).splice(this._selectedNum, 1, value);
@@ -492,6 +544,7 @@ export class CollectionSchemaListCell extends CollectionSchemaCell {
@action
onSelected = (element: string, index: number) => {
+ // if an item is selected, the private variables should update to reflect this
this._text = element;
this._selectedNum = index;
}
@@ -505,6 +558,7 @@ export class CollectionSchemaListCell extends CollectionSchemaCell {
const link = false;
const reference = React.createRef<HTMLDivElement>();
+ // if the list is not opened, don't display it; otherwise, do.
if (this._optionsList?.length) {
const options = !this._opened ? (null) :
<div>
@@ -572,6 +626,7 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
@observer
export class CollectionSchemaButtons extends CollectionSchemaCell {
+ // the navigation buttons for schema view when it is used for search.
render() {
return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <></> :
<div style={{ paddingTop: 8, paddingLeft: 3 }} >
@@ -583,4 +638,4 @@ export class CollectionSchemaButtons extends CollectionSchemaCell {
</button>
</div>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
index a25f962df..1306b79cb 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
@@ -1,18 +1,17 @@
import React = require("react");
-import { IconProp, library } from "@fortawesome/fontawesome-svg-core";
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction } from "mobx";
+import { action, computed, observable, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, Opt } from "../../../../fields/Doc";
+import { Doc, DocListCast, Opt, StrListCast } from "../../../../fields/Doc";
import { listSpec } from "../../../../fields/Schema";
import { PastelSchemaPalette, SchemaHeaderField } from "../../../../fields/SchemaHeaderField";
import { ScriptField } from "../../../../fields/ScriptField";
import { Cast, StrCast } from "../../../../fields/Types";
import { undoBatch } from "../../../util/UndoManager";
-import { SearchBox } from "../../search/SearchBox";
+import { CollectionView } from "../CollectionView";
import { ColumnType } from "./CollectionSchemaView";
import "./CollectionSchemaView.scss";
-import { CollectionView } from "../CollectionView";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
@@ -25,14 +24,14 @@ export interface AddColumnHeaderProps {
@observer
export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> {
+ // the button that allows the user to add a column
render() {
- return (
- <button className="add-column" onClick={() => this.props.createColumn()}><FontAwesomeIcon icon="plus" size="sm" /></button>
- );
+ return <button className="add-column" onClick={() => this.props.createColumn()}>
+ <FontAwesomeIcon icon="plus" size="sm" />
+ </button>;
}
}
-
export interface ColumnMenuProps {
columnField: SchemaHeaderField;
// keyValue: string;
@@ -234,7 +233,7 @@ export interface KeysDropdownProps {
@observer
export class KeysDropdown extends React.Component<KeysDropdownProps> {
@observable private _key: string = this.props.keyValue;
- @observable private _searchTerm: string = this.props.keyValue;
+ @observable private _searchTerm: string = this.props.keyValue + ":";
@observable private _isOpen: boolean = false;
@observable private _node: HTMLDivElement | null = null;
@observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef();
@@ -326,11 +325,11 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
|| ((!key.startsWith("_") && key[0] === key[0].toUpperCase()) || key[0] === "#")) ? showKeys.add(key) : null);
return Array.from(showKeys.keys()).filter(key => !this._searchTerm || key.includes(this._searchTerm));
}
- @action
- renderOptions = (): JSX.Element[] | JSX.Element => {
+
+ @computed get renderOptions() {
if (!this._isOpen) {
this.defaultMenuHeight = 0;
- return <></>;
+ return (null);
}
const options = this.showKeys.map(key => {
return <div key={key} className="key-option" style={{
@@ -373,44 +372,39 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
return options;
}
- docSafe: Doc[] = [];
+ @computed get docSafe() { return DocListCast(this.props.dataDoc?.[this.props.fieldKey]); }
- @action
- renderFilterOptions = (): JSX.Element[] | JSX.Element => {
+ @computed get renderFilterOptions() {
if (!this._isOpen || !this.props.dataDoc) {
this.defaultMenuHeight = 0;
- return <></>;
+ return (null);
}
const keyOptions: string[] = [];
const colpos = this._searchTerm.indexOf(":");
const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length);
- if (this.docSafe.length === 0) {
- this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]);
- }
- const docs = this.docSafe;
- docs.forEach((doc) => {
+ this.docSafe.forEach(doc => {
const key = StrCast(doc[this._key]);
if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") {
keyOptions.push(key);
}
});
- const filters = Cast(this.props.Document._docFilters, listSpec("string"));
- if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) {
+ const filters = StrListCast(this.props.Document._docFilters);
+ if (filters.some(filter => filter.split(":")[0] === this._key) === false) {
this.props.col.setColor("rgb(241, 239, 235)");
this.closeResultsVisibility = "none";
}
for (let i = 0; i < (filters?.length ?? 0) - 1; i++) {
- if (filters![i] === this.props.col.heading && keyOptions.includes(filters![i].split(":")[1]) === false) {
- keyOptions.push(filters![i + 1]);
+ if (filters[i] === this.props.col.heading && keyOptions.includes(filters[i].split(":")[1]) === false) {
+ keyOptions.push(filters[i + 1]);
}
}
const options = keyOptions.map(key => {
let bool = false;
if (filters !== undefined) {
- const ind = filters.findIndex(filter => filter.split(":")[0] === key);
+ const ind = filters.findIndex(filter => filter.split(":")[1] === key);
const fields = ind === -1 ? undefined : filters[ind].split(":");
- bool = fields ? fields[1] === "check" : false;
+ bool = fields ? fields[2] === "check" : false;
}
return <div key={key} className="key-option" style={{
paddingLeft: 5, textAlign: "left",
@@ -420,12 +414,16 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
<input type="checkbox"
onPointerDown={e => e.stopPropagation()}
onClick={e => e.stopPropagation()}
- onChange={(e) => {
- e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, "remove");
- e.target.checked === true ? this.closeResultsVisibility = "contents" : console.log("");
- e.target.checked === true ? this.props.col.setColor("green") : this.updateFilter();
- e.target.checked === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove");
- }}
+ onChange={action(e => {
+ if (e.target.checked) {
+ Doc.setDocFilter(this.props.Document, this._key, key, "check");
+ this.closeResultsVisibility = "contents";
+ this.props.col.setColor("green");
+ } else {
+ Doc.setDocFilter(this.props.Document, this._key, key, "remove");
+ this.updateFilter();
+ }
+ })}
checked={bool}
/>
<span style={{ paddingLeft: 4 }}>
@@ -472,11 +470,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
removeFilters = (e: React.PointerEvent): void => {
const keyOptions: string[] = [];
- if (this.docSafe.length === 0 && this.props.dataDoc) {
- this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]);
- }
- const docs = this.docSafe;
- docs.forEach((doc) => {
+ this.docSafe.forEach(doc => {
const key = StrCast(doc[this._key]);
if (keyOptions.includes(key) === false) {
keyOptions.push(key);
@@ -494,10 +488,6 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
<FontAwesomeIcon icon={this.props.icon} size="lg" style={{ display: "inline" }} />
</div>
- {/* <FontAwesomeIcon icon={fa.faSearchMinus} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} onClick={e => {
- runInAction(() => { this._isOpen === undefined ? this._isOpen = true : this._isOpen = !this._isOpen })
- }} /> */}
-
<div className="keys-dropdown" style={{ zIndex: 1, width: this.props.width, maxWidth: this.props.width }}>
<input className="keys-search" style={{ width: "100%" }}
ref={this._inputRef} type="text" value={this._searchTerm} placeholder="Column key" onKeyDown={this.onKeyDown}
@@ -511,7 +501,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
{!this._isOpen ? (null) : <div className="keys-options-wrapper" style={{
width: this.props.width, maxWidth: this.props.width, height: "auto",
}}>
- {this._searchTerm.includes(":") ? this.renderFilterOptions() : this.renderOptions()}
+ {this._searchTerm.includes(":") ? this.renderFilterOptions : this.renderOptions}
</div>}
</div >
</div>
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx
index 456c38c68..2df95ffd8 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx
@@ -21,27 +21,38 @@ export interface MovableColumnProps {
ScreenToLocalTransform: () => Transform;
}
export class MovableColumn extends React.Component<MovableColumnProps> {
+ // The header of the column
private _header?: React.RefObject<HTMLDivElement> = React.createRef();
+ // The container of the function that is responsible for moving the column over to a new plac
private _colDropDisposer?: DragManager.DragDropDisposer;
+ // initial column position
private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 };
+ // sensitivity to being dragged, in pixels
private _sensitivity: number = 16;
+ // Column reference ID
private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
onPointerEnter = (e: React.PointerEvent): void => {
+ // if the column is left-clicked and it is being dragged
if (e.buttons === 1 && SnappingManager.GetIsDragging()) {
this._header!.current!.className = "collectionSchema-col-wrapper";
document.addEventListener("pointermove", this.onDragMove, true);
}
}
+
onPointerLeave = (e: React.PointerEvent): void => {
this._header!.current!.className = "collectionSchema-col-wrapper";
document.removeEventListener("pointermove", this.onDragMove, true);
!e.buttons && document.removeEventListener("pointermove", this.onPointerMove);
}
+
onDragMove = (e: PointerEvent): void => {
+ // only take into account the horizonal direction when a column is dragged
const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
const rect = this._header!.current!.getBoundingClientRect();
+ // Now store the point at the top center of the column when it was in its original position
const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top);
+ // to be compared with its new horizontal position
const before = x[0] < bounds[0];
this._header!.current!.className = "collectionSchema-col-wrapper";
if (before) this._header!.current!.className += " col-before";
@@ -58,11 +69,15 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
colDrop = (e: Event, de: DragManager.DropEvent) => {
document.removeEventListener("pointermove", this.onDragMove, true);
+ // we only care about whether the column is shifted to the side
const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ // get the dimensions of the smallest rectangle that bounds the header
const rect = this._header!.current!.getBoundingClientRect();
const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top);
+ // get whether the column was dragged before or after where it is now
const before = x[0] < bounds[0];
const colDragData = de.complete.columnDragData;
+ // if there is colDragData, which happen when the drag is complete, reorder the columns according to the established variables
if (colDragData) {
e.stopPropagation();
this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns);
@@ -85,8 +100,10 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
};
+ // if the left mouse button is the one being held
if (e.buttons === 1) {
const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
+ // If the movemnt of the drag exceeds the sensitivity value
if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
document.removeEventListener("pointermove", this.onPointerMove);
e.stopPropagation();
@@ -105,6 +122,7 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
onPointerDown = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
this._dragRef = ref;
const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY);
+ // If the cell thing dragged is not being edited
if (!(e.target as any)?.tagName.includes("INPUT")) {
this._startDragPosition = { x: dx, y: dy };
document.addEventListener("pointermove", this.onPointerMove);
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 3074ce66e..b64e9dac1 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -132,6 +132,13 @@
min-height: 30px;
border: 0 !important;
}
+ .rt-tr-group:nth-of-type(even) {
+ direction: ltr;
+ flex: 0 1 auto;
+ min-height: 30px;
+ border: 0 !important;
+ background-color: red;
+ }
.rt-tr {
width: 100%;
min-height: 30px;
@@ -444,11 +451,12 @@ button.add-column {
border: none;
background-color: $white;
width: 100%;
- height: 100%;
+ height: fit-content;
min-height: 26px;
}
}
&.focused {
+ overflow: hidden;
&.inactive {
border: none;
}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index dfe99ffc8..b89246489 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,6 +1,6 @@
import React = require("react");
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked } from "mobx";
+import { action, computed, observable, untracked, trace } from "mobx";
import { observer } from "mobx-react";
import Measure from "react-measure";
import { Resize } from "react-table";
@@ -337,8 +337,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
{this.renderTypes(this._col)}
{this.renderColors(this._col)}
<div className="collectionSchema-headerMenu-group">
- <button onClick={() => { this.deleteColumn(this._col.heading); }}
- >Hide Column</button>
+ <button onClick={() => { this.deleteColumn(this._col.heading); }}>
+ Hide Column
+ </button>
</div>
</div>;
}
@@ -353,7 +354,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@action setFocused = (doc: Doc) => this._focusedTable = doc;
@action setPreviewDoc = (doc: Opt<Doc>) => {
- SelectionManager.SelectSchemaView(this, doc);
+ SelectionManager.SelectSchemaViewDoc(doc);
this._previewDoc = doc;
}
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index 3833f968b..bc5a9559f 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -1,7 +1,7 @@
import React = require("react");
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, trace } from "mobx";
import { observer } from "mobx-react";
import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table";
import "react-table/react-table.css";
@@ -386,13 +386,13 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
@undoBatch
@action
createColumn = () => {
- let index = 0;
- let found = this.props.columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1;
- while (found) {
- index++;
- found = this.props.columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1;
+ const newFieldName = (index: number) => `New field${index ? ` (${index})` : ""}`;
+ for (let index = 0; index < 100; index++) {
+ if (this.props.columns.findIndex(col => col.heading === newFieldName(index)) === -1) {
+ this.props.columns.push(new SchemaHeaderField(newFieldName(index), "#f1efeb"));
+ break;
+ }
}
- this.props.columns.push(new SchemaHeaderField(`New field ${index ? "(" + index + ")" : ""}`, "#f1efeb"));
}
@action
@@ -567,7 +567,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
<div className="collectionSchemaView-documentPreview" ref="overlay"
style={{
position: "absolute", width: 150, height: 150,
- background: "dimGray", display: "block", top: 0, left: 0,
+ background: "dimgray", display: "block", top: 0, left: 0,
transform: `translate(${this._showDocPos[0]}px, ${this._showDocPos[1] - 180}px)`
}} >
<DocumentView
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index 95bd44c1f..520ac9357 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -8,6 +8,7 @@ $dark-gray: #323232;
$black: #000000;
$light-blue: #bdddf5;
+$light-blue-transparent: #bdddf590;
$medium-blue: #4476f7;
$medium-blue-alt: #4476f73d;
$pink: #e0217d;
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index 240a71c3e..db331bb75 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -2,13 +2,14 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from "@material-ui/core";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, StrListCast } from "../../../fields/Doc";
-import { DateCast, StrCast } from "../../../fields/Types";
+import { Doc, NumListCast, StrListCast, Field } from "../../../fields/Doc";
+import { DateCast, StrCast, Cast } from "../../../fields/Types";
import { LinkManager } from "../../util/LinkManager";
import { undoBatch } from "../../util/UndoManager";
import './LinkEditor.scss';
import { LinkRelationshipSearch } from "./LinkRelationshipSearch";
import React = require("react");
+import { ToString } from "../../../fields/FieldSymbols";
interface LinkEditorProps {
@@ -20,7 +21,7 @@ interface LinkEditorProps {
@observer
export class LinkEditor extends React.Component<LinkEditorProps> {
- @observable description = StrCast(LinkManager.currentLink?.description);
+ @observable description = Field.toString(LinkManager.currentLink?.description as any as Field);
@observable relationship = StrCast(LinkManager.currentLink?.linkRelationship);
@observable openDropdown: boolean = false;
@observable showInfo: boolean = false;
@@ -41,14 +42,36 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
@undoBatch
setRelationshipValue = action((value: string) => {
if (LinkManager.currentLink) {
+ const prevRelationship = LinkManager.currentLink.linkRelationship as string;
LinkManager.currentLink.linkRelationship = value;
+ Doc.GetProto(LinkManager.currentLink).linkRelationship = value;
const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
+ const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes);
const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
+
// if the relationship does not exist in the list, add it and a corresponding unique randomly generated color
- if (linkRelationshipList && !linkRelationshipList.includes(value)) {
+ if (!linkRelationshipList?.includes(value)) {
linkRelationshipList.push(value);
+ linkRelationshipSizes.push(1);
const randColor = "rgb(" + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + ")";
linkColorList.push(randColor);
+ // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes
+ } else if (linkRelationshipList && value !== prevRelationship) {
+ const index = linkRelationshipList.indexOf(value);
+ //increment size of new relationship size
+ if (index !== -1 && index < linkRelationshipSizes.length) {
+ const pvalue = linkRelationshipSizes[index];
+ linkRelationshipSizes[index] = (pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1);
+ }
+ //decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation)
+ if (linkRelationshipList.includes(prevRelationship)) {
+ const pindex = linkRelationshipList.indexOf(prevRelationship);
+ if (pindex !== -1 && pindex < linkRelationshipSizes.length) {
+ const pvalue = linkRelationshipSizes[pindex];
+ linkRelationshipSizes[pindex] = Math.max(0, (pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue - 1));
+ }
+ }
+
}
this.relationshipButtonColor = "rgb(62, 133, 55)";
setTimeout(action(() => this.relationshipButtonColor = ""), 750);
@@ -79,7 +102,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
@undoBatch
setDescripValue = action((value: string) => {
if (LinkManager.currentLink) {
- LinkManager.currentLink.description = value;
+ Doc.GetProto(LinkManager.currentLink).description = value;
this.buttonColor = "rgb(62, 133, 55)";
setTimeout(action(() => this.buttonColor = ""), 750);
return true;
@@ -140,6 +163,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
style={{ width: "100%" }}
id="input"
value={this.relationship}
+ autoComplete={"off"}
placeholder={"Enter link relationship"}
onKeyDown={this.onRelationshipKey}
onChange={this.handleRelationshipChange}
@@ -168,6 +192,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
<div className="linkEditor-description-editing">
<input
style={{ width: "100%" }}
+ autoComplete={"off"}
id="input"
value={this.description}
placeholder={"Enter link description"}
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index cb6571f92..03377ad4e 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -31,7 +31,6 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
if (RGBcolor) {
//set opacity to 0.25 by modifiying the rgb string
color = RGBcolor.slice(0, RGBcolor.length - 1) + ", 0.25)";
- console.log(color);
}
}
return color;
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 53a7ae9ab..96cc6d600 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -79,7 +79,9 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
onEdit = (e: React.PointerEvent): void => {
LinkManager.currentLink = this.props.linkDoc;
setupMoveUpEvents(this, e, e => {
- DragManager.StartDocumentDrag([this._editRef.current!], new DragManager.DocumentDragData([this.props.linkDoc]), e.x, e.y);
+ const dragData = new DragManager.DocumentDragData([this.props.linkDoc], "alias");
+ dragData.removeDropProperties = ["hidden"];
+ DragManager.StartDocumentDrag([this._editRef.current!], dragData, e.x, e.y);
return true;
}, emptyFunction, () => this.props.showEditor(this.props.linkDoc));
}
@@ -163,19 +165,19 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
<div className="linkMenu-item-buttons" ref={this._buttonRef} >
- <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show Anchor" : "Hide Anchor"}</div></>}>
- <div className="button" ref={this._editRef} onPointerDown={this.showAnchor} onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={this.props.linkDoc.hidden ? "eye-slash" : "eye"} size="sm" /></div>
+ <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show Link Anchor" : "Hide Link Anchor"}</div></>}>
+ <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "" : "#4476f7" /* $medium-blue */ }} onPointerDown={this.showAnchor} onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon={"eye"} size="sm" /></div>
</Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{!this.props.linkDoc.linkDisplay ? "Show link" : "Hide link"}</div></>}>
- <div className="button" ref={this._editRef} onPointerDown={this.showLink} onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={!this.props.linkDoc.linkDisplay ? "eye-slash" : "eye"} size="sm" /></div>
+ <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? "Hide Link Line" : "Show Link Line"}</div></>}>
+ <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "gray" : this.props.linkDoc.linkDisplay ? "#4476f7"/* $medium-blue */ : "" }} onPointerDown={this.showLink} onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon={"project-diagram"} size="sm" /></div>
</Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{!this.props.linkDoc.linkAutoMove ? "Auto move dot" : "Freeze dot position"}</div></>}>
- <div className="button" ref={this._editRef} onPointerDown={this.autoMove} onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={this.props.linkDoc.linkAutoMove ? "play" : "pause"} size="sm" /></div>
+ <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? "Click to freeze link anchor position" : "Click to auto move link anchor"}</div></>}>
+ <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "gray" : !this.props.linkDoc.linkAutoMove ? "" : "#4476f7" /* $medium-blue */ }} onPointerDown={this.autoMove} onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon={"play"} size="sm" /></div>
</Tooltip>
<Tooltip title={<><div className="dash-tooltip">Edit Link</div></>}>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 9cc4b1f9a..fe34d6687 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -7,7 +7,7 @@ import { listSpec } from "../../../fields/Schema";
import { ComputedField } from "../../../fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { TraceMobx } from "../../../fields/util";
-import { numberRange } from "../../../Utils";
+import { DashColor, numberRange } from "../../../Utils";
import { DocumentManager } from "../../util/DocumentManager";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
@@ -18,7 +18,6 @@ import { StyleProp } from "../StyleProvider";
import "./CollectionFreeFormDocumentView.scss";
import { DocumentView, DocumentViewProps } from "./DocumentView";
import React = require("react");
-import Color = require("color");
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: number, highlight?: boolean, z: number, transition?: string } | undefined;
@@ -165,7 +164,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
PanelHeight: this.panelHeight,
};
const background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (background && Color(background).alpha() !== 1 ? "multiply" : undefined);
+ const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (typeof background === "string" && DashColor(background).alpha() !== 1 ? "multiply" : undefined);
return <div className={"collectionFreeFormDocumentView-container"}
style={{
outline: this.Highlight ? "orange solid 2px" : "",
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index 44cf5d046..660045a6f 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -6,6 +6,7 @@
position: relative;
z-index: 0;
pointer-events: none;
+ display: flex;
.clip-div {
position: absolute;
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 6708a08ee..750213e67 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -15,6 +15,7 @@ import "./ComparisonBox.scss";
import { DocumentView, DocumentViewProps } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import React = require("react");
+import { DocumentType } from '../../documents/DocumentTypes';
export const comparisonSchema = createSchema({});
@@ -49,7 +50,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
}
private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => {
- setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => {
+ e.button !== 2 && setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => {
// on click, animate slider movement to the targetWidth
this._animating = "all 200ms";
this.layoutDoc._clipWidth = targetWidth * 100 / this.props.PanelWidth();
@@ -87,14 +88,21 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
</div>;
};
const displayDoc = (which: string) => {
- const whichDoc = Cast(this.dataDoc[`compareBox-${which}`], Doc, null);
+ const whichDoc = Cast(this.dataDoc[which], Doc, null);
+ //if (whichDoc?.type === DocumentType.MARKER)
+ const targetDoc = Cast(whichDoc?.annotationOn, Doc, null) ?? whichDoc;
return whichDoc ? <>
- <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+ <DocumentView
+ ref={(r) => {
+ whichDoc !== targetDoc && r?.focus(targetDoc);
+ }}
+ {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
isContentActive={returnFalse}
isDocumentActive={returnFalse}
styleProvider={this.docStyleProvider}
- Document={whichDoc}
+ Document={targetDoc}
DataDoc={undefined}
+ hideLinkButton={true}
pointerEvents={"none"} />
{clearButton(which)}
</> : // placeholder image if doc is missing
@@ -105,16 +113,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
const displayBox = (which: string, index: number, cover: number) => {
return <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }}
onPointerDown={e => this.registerSliding(e, cover)}
- ref={ele => this.createDropTarget(ele, `compareBox-${which}`, index)} >
+ ref={ele => this.createDropTarget(ele, which, index)} >
{displayDoc(which)}
</div>;
};
return (
<div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? "-interactive" : ""}` /* change className to easily disable/enable pointer events in CSS */}>
- {displayBox("after", 1, this.props.PanelWidth() - 3)}
+ {displayBox(this.fieldKey === "data" ? "compareBox-after" : `${this.fieldKey}2`, 1, this.props.PanelWidth() - 3)}
<div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, "gray") }}>
- {displayBox("before", 0, 0)}
+ {displayBox(this.fieldKey === "data" ? "compareBox-before" : `${this.fieldKey}1`, 0, 0)}
</div>
<div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)`, cursor: NumCast(this.layoutDoc._clipWidth) < 5 ? "e-resize" : NumCast(this.layoutDoc._clipWidth) / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? "w-resize" : undefined }}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 7f164ca48..1ec7bf72a 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -175,6 +175,7 @@
position: absolute;
bottom: 0;
width: 100%;
+ overflow-y: scroll;
transform-origin: bottom left;
opacity: 0.1;
transition: opacity 0.5s;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e8a78d75c..949e0e168 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -13,7 +13,7 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Ty
import { AudioField } from "../../../fields/URLField";
import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
import { MobileInterface } from '../../../mobile/MobileInterface';
-import { emptyFunction, hasDescendantTarget, OmitKeys, returnTrue, returnVal, Utils, lightOrDark, simulateMouseClick } from "../../../Utils";
+import { emptyFunction, hasDescendantTarget, OmitKeys, returnTrue, returnVal, Utils, lightOrDark, simulateMouseClick, returnEmptyString } from "../../../Utils";
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentType } from '../../documents/DocumentTypes';
@@ -25,7 +25,6 @@ import { InteractionUtils } from '../../util/InteractionUtils';
import { LinkManager } from '../../util/LinkManager';
import { Scripting } from '../../util/Scripting';
import { SelectionManager } from "../../util/SelectionManager";
-import { ColorScheme } from "../../util/SettingsManager";
import { SharingManager } from '../../util/SharingManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from "../../util/Transform";
@@ -50,6 +49,7 @@ import { ScriptingBox } from "./ScriptingBox";
import { PresBox } from './trails/PresBox';
import React = require("react");
import { IconProp } from "@fortawesome/fontawesome-svg-core";
+import { ColorScheme } from "../../util/SettingsManager";
const { Howl } = require('howler');
interface Window {
@@ -90,12 +90,14 @@ export interface DocComponentView {
getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
playFrom?: (time: number, endTime?: number) => void;
+ Pause?: () => void;
setFocus?: () => void;
- componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element;
+ componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
fieldKey?: string;
annotationKey?: string;
getTitle?: () => string;
getScrollHeight?: () => number;
+ search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
}
export interface DocumentViewSharedProps {
renderDepth: number;
@@ -486,8 +488,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (this.onDoubleClickHandler) {
this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350);
} else clickFunc();
- } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself
- this.props.addDocTab(DocUtils.makeCustomViewClicked(Doc.MakeAlias(this.props.Document), undefined, "onClick"), "add:right");
} else if (this.allLinks && this.Document.type !== DocumentType.LINK && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
this.allLinks.length && LinkManager.FollowLink(undefined, this.props.Document, this.props, e.altKey);
} else {
@@ -555,10 +555,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
}
- onPointerUp = (e: PointerEvent): void => {
+ cleanupPointerEvents = () => {
this.cleanUpInteractions();
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ onPointerUp = (e: PointerEvent): void => {
+ this.cleanupPointerEvents();
if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
@@ -577,8 +581,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (this.Document._isLinkButton && !this.onClickHandler) {
this.Document.followLinkZoom = zoom;
this.Document.followLinkLocation = location;
- } else {
- this.Document.onClick = this.layoutDoc.onClick = undefined;
+ } else if (this.Document._isLinkButton && this.onClickHandler) {
+ this.Document._isLinkButton = false;
+ this.Document["onClick-rawScript"] = this.dataDoc["onClick-rawScript"] = this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined;
}
}
@undoBatch @action
@@ -797,12 +802,22 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
}
+ collectionFilters = () => StrListCast(this.props.Document._docFilters);
+ collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters);
+ @computed get showFilterIcon() {
+ return this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" :
+ this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined;
+ }
rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
panelHeight = () => this.props.PanelHeight() - this.headerMargin;
screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
contentScaling = () => this.ContentScale;
onClickFunc = () => this.onClickHandler;
- setHeight = (height: number) => this.layoutDoc._height = height;
+ setHeight = (height: number) => {
+ if (this.props.renderDepth !== -1) {
+ this.layoutDoc._height = height;
+ }
+ }
setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view);
isContentActive = (outsideReaction?: boolean) => {
return CurrentUserUtils.SelectedTool !== InkTool.None ||
@@ -827,7 +842,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>;
return <div className="documentView-contentsView"
style={{
- pointerEvents: this.rootDoc.type !== DocumentType.INK && ((this.props.contentPointerEvents as any) || (this.isContentActive())) ? "all" : "none",
+ pointerEvents: this.props.pointerEvents as any ? this.props.pointerEvents as any : (this.rootDoc.type !== DocumentType.INK && ((this.props.contentPointerEvents as any) || (this.isContentActive())) ? "all" : "none"),
height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
}}>
<DocumentContentsView key={1} {...this.props}
@@ -843,7 +858,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
focus={this.focus}
layoutKey={this.finalLayoutKey} />
{this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints}
- {this.hideLinkButton ? (null) :
+ {this.hideLinkButton || this.props.renderDepth === -1 ? (null) :
<div style={{ transformOrigin: "top left", transform: `scale(${Math.min(1, this.props.ScreenToLocalTransform().scale(this.props.ContentScaling?.() || 1).Scale)})` }}>
<DocumentLinksButton View={this.props.DocumentView()} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />
</div>}
@@ -864,6 +879,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
anchorPanelHeight = () => this.props.PanelHeight() || 1;
anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
switch (property) {
+ case StyleProp.ShowTitle: return "";
case StyleProp.PointerEvents: return "none";
case StyleProp.LinkSource: return this.props.Document;// pass the LinkSource to the LinkAnchorBox
default: return this.props.styleProvider?.(doc, props, property);
@@ -896,6 +912,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
PanelWidth={this.anchorPanelWidth}
PanelHeight={this.anchorPanelHeight}
dontRegisterView={false}
+ showTitle={returnEmptyString}
+ hideCaptions={true}
fitWidth={returnTrue}
styleProvider={this.anchorStyleProvider}
removeDocument={this.hideLinkAnchor}
@@ -958,19 +976,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ":caption");
@computed get innards() {
TraceMobx();
+ const ffscale = (this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1);
const showTitle = this.ShowTitle?.split(":")[0];
const showTitleHover = this.ShowTitle?.includes(":hover");
const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined;
const captionView = !showCaption ? (null) :
<div className="documentView-captionWrapper"
- style={{ pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined, }}>
+ style={{
+ pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined,
+ minWidth: 50 * ffscale,
+ maxHeight: `max(100%, ${20 * ffscale}px)`
+ }}>
<FormattedTextBox {...OmitKeys(this.props, ['children']).omit}
yPadding={10}
xPadding={10}
fieldKey={showCaption}
- fontSize={Math.min(32, 12 * this.props.ScreenToLocalTransform().Scale)}
+ fontSize={12 * Math.max(1, 2 * ffscale / 3)}
styleProvider={this.captionStyleProvider}
dontRegisterView={true}
+ noSidebar={true}
+ dontScale={true}
isContentActive={this.isContentActive}
onClick={this.onClickFunc}
/>
@@ -1040,9 +1065,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
render() {
TraceMobx();
const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : 0) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
- const highlightColor = (Doc.UserDoc().colorScheme === ColorScheme.Dark ?
- ["transparent", "#65350c", "#65350c", "yellow", "magenta", "cyan", "orange"] :
- ["transparent", "#4476F7", "#4476F7", "yellow", "magenta", "cyan", "orange"])[highlightIndex];
+ const highlightColor = ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex];
const highlightStyle = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"][highlightIndex];
const excludeTypes = !this.props.treeViewDoc ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON];
let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear;
@@ -1082,6 +1105,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>
</>
}
+ {this.showFilterIcon ?
+ <FontAwesomeIcon icon={"filter"} size="lg"
+ style={{ position: 'absolute', top: '1%', right: '1%', cursor: "pointer", padding: 1, color: this.showFilterIcon === "hasFilter" ? '#18c718bd' : "orange", zIndex: 1 }}
+ onPointerDown={action(e => { this.props.select(false); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })}
+ />
+ : (null)}
</div>;
}
}
@@ -1122,10 +1151,10 @@ export class DocumentView extends React.Component<DocumentViewProps> {
@computed get nativeScaling() {
if (this.shouldNotScale) return 1;
const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0;
- if (this.fitWidth || this.props.PanelHeight() / this.effectiveNativeHeight > this.props.PanelWidth() / this.effectiveNativeWidth) {
- return Math.max(minTextScale, this.props.PanelWidth() / this.effectiveNativeWidth); // width-limited or fitWidth
+ if (this.fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
+ return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth
}
- return Math.max(minTextScale, this.props.PanelHeight() / this.effectiveNativeHeight); // height-limited or unscaled
+ return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
}
@computed get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); }
@@ -1148,7 +1177,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
const xf = (this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling)).inverse();
const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)];
- if (this.docView.props.LayoutTemplateString?.includes("LinkAnchorBox")) {
+ if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
const docuBox = this.docView.ContentDiv.getElementsByClassName("linkAnchorBox-cont");
if (docuBox.length) return docuBox[0].getBoundingClientRect();
}
diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss
index e69de29bb..6c9d53d10 100644
--- a/src/client/views/nodes/EquationBox.scss
+++ b/src/client/views/nodes/EquationBox.scss
@@ -0,0 +1,3 @@
+.equationBox-cont {
+ transform-origin: top left;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index dacafcdf4..f1f802c13 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -84,10 +84,16 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps, EquationDo
}
render() {
TraceMobx();
- return (<div onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
+ const scale = (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
+ return (<div className="equationBox-cont"
+ onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
style={{
+ transform: `scale(${scale})`,
+ width: `${100 / scale}%`,
+ height: `${100 / scale}%`,
pointerEvents: !this.props.isSelected() ? "none" : undefined,
}}
+ onKeyDown={e => e.stopPropagation()}
>
<EquationEditor ref={this._ref}
value={this.dataDoc.text || "x"}
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index e9f19bf9e..2041c7399 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -377,9 +377,9 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
</div>
<div className="filterBox-select-bool">
- <select className="filterBox-selection" onChange={this.changeBool}>
- <option value="AND" key="AND" selected={(FilterBox.targetDoc.currentFilter as Doc)?.filterBoolean === "AND"}>AND</option>
- <option value="OR" key="OR" selected={(FilterBox.targetDoc.currentFilter as Doc)?.filterBoolean === "OR"}>OR</option>
+ <select className="filterBox-selection" onChange={this.changeBool} defaultValue={StrCast((FilterBox.targetDoc.currentFilter as Doc)?.filterBoolean)}>
+ <option value="AND" key="AND">AND</option>
+ <option value="OR" key="OR">OR</option>
</select>
<div className="filterBox-select-text">filters together</div>
</div>
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index b00f97236..5050fc2d2 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -1,18 +1,16 @@
-import EquationEditor from 'equation-editor-react';
import functionPlot from "function-plot";
+import { action, computed, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
+import { Doc, DocListCast } from '../../../fields/Doc';
import { documentSchema } from '../../../fields/documentSchemas';
-import { createSchema, makeInterface, listSpec } from '../../../fields/Schema';
-import { StrCast, Cast } from '../../../fields/Types';
+import { List } from '../../../fields/List';
+import { createSchema, listSpec, makeInterface } from '../../../fields/Schema';
+import { Cast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
+import { Docs } from '../../documents/Documents';
import { ViewBoxBaseComponent } from '../DocComponent';
import { FieldView, FieldViewProps } from './FieldView';
-import './LabelBox.scss';
-import { DocListCast, Doc } from '../../../fields/Doc';
-import { computed, action, reaction } from 'mobx';
-import { Docs } from '../../documents/Documents';
-import { List } from '../../../fields/List';
const EquationSchema = createSchema({});
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 2536dbe16..4238f6d29 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -13,6 +13,7 @@
width: 100%;
pointer-events: none;
mix-blend-mode: multiply; // bcz: makes text fuzzy!
+ overflow: hidden;
}
.imageBox-fader {
@@ -101,7 +102,6 @@
margin: 0 auto;
display: flex;
height: 100%;
- overflow: auto;
.imageBox-fadeBlocker {
width: 100%;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index b41bfd3ea..89f70985c 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -81,11 +81,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}),
({ forceFull, scrSize, selected }) => this._curSuffix = forceFull ? "_o" : scrSize < 100 ? "_s" : scrSize < 400 ? "_m" : scrSize < 800 || !selected ? "_l" : "_o",
{ fireImmediately: true, delay: 1000 });
- this._disposers.selection = reaction(() => this.props.isSelected(),
- selected => !selected && setTimeout(() => {
- // Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove()));
- // this._savedAnnotations.clear();
- }));
this._disposers.path = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }),
({ nativeSize, width }) => {
if (!this.layoutDoc._height) {
@@ -291,7 +286,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
- <div className="imageBox-fader" >
+ <div className="imageBox-fader" style={{ overflow: this.props.docViewPath?.().lastElement().fitWidth ? "auto" : undefined }} >
<img key="paths" ref={this._imgRef}
src={srcpath}
style={{ transform, transformOrigin }}
@@ -359,15 +354,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}} >
<CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
renderDepth={this.props.renderDepth + 1}
- isAnnotationOverlay={true}
fieldKey={this.annotationKey}
CollectionView={undefined}
+ isAnnotationOverlay={true}
annotationLayerHostsContent={true}
PanelWidth={this.props.PanelWidth}
PanelHeight={this.props.PanelHeight}
ScreenToLocalTransform={this.screenToLocalTransform}
- scaling={returnOne}
select={emptyFunction}
+ scaling={returnOne}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
diff --git a/src/client/views/nodes/LabelBigText.js b/src/client/views/nodes/LabelBigText.js
new file mode 100644
index 000000000..db43551d8
--- /dev/null
+++ b/src/client/views/nodes/LabelBigText.js
@@ -0,0 +1,241 @@
+/*
+Brorlandi/big-text.js v1.0.0, 2017
+Adapted from DanielHoffmann/jquery-bigtext, v1.3.0, May 2014
+And from Jetroid/bigtext.js v1.0.0, September 2016
+
+Usage:
+BigText("#myElement",{
+ rotateText: {Number}, (null)
+ fontSizeFactor: {Number}, (0.8)
+ maximumFontSize: {Number}, (null)
+ limitingDimension: {String}, ("both")
+ horizontalAlign: {String}, ("center")
+ verticalAlign: {String}, ("center")
+ textAlign: {String}, ("center")
+ whiteSpace: {String}, ("nowrap")
+});
+
+
+Original Projects:
+https://github.com/DanielHoffmann/jquery-bigtext
+https://github.com/Jetroid/bigtext.js
+
+Options:
+
+rotateText: Rotates the text inside the element by X degrees.
+
+fontSizeFactor: This option is used to give some vertical spacing for letters that overflow the line-height (like 'g', 'Á' and most other accentuated uppercase letters). This does not affect the font-size if the limiting factor is the width of the parent div. The default is 0.8
+
+maximumFontSize: maximum font size to use.
+
+limitingDimension: In which dimension the font size should be limited. Possible values: "width", "height" or "both". Defaults to both. Using this option with values different than "both" overwrites the element parent width or height.
+
+horizontalAlign: Where to align the text horizontally. Possible values: "left", "center", "right". Defaults to "center".
+
+verticalAlign: Where to align the text vertically. Possible values: "top", "center", "bottom". Defaults to "center".
+
+textAlign: Sets the text align of the element. Possible values: "left", "center", "right". Defaults to "center". This option is only useful if there are linebreaks (<br> tags) inside the text.
+
+whiteSpace: Sets whitespace handling. Possible values: "nowrap", "pre". Defaults to "nowrap". (Can also be set to enable wrapping but this doesn't work well.)
+
+Bruno Orlandi - 2017
+
+Copyright (C) 2013 Daniel Hoffmann Bernardes, Ícaro Technologies
+Copyright (C) 2016 Jet Holt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+function _calculateInnerDimensions(computedStyle) {
+ //Calculate the inner width and height
+ var innerWidth;
+ var innerHeight;
+
+ var width = parseInt(computedStyle.getPropertyValue("width"));
+ var height = parseInt(computedStyle.getPropertyValue("height"));
+ var paddingLeft = parseInt(computedStyle.getPropertyValue("padding-left"));
+ var paddingRight = parseInt(computedStyle.getPropertyValue("padding-right"));
+ var paddingTop = parseInt(computedStyle.getPropertyValue("padding-top"));
+ var paddingBottom = parseInt(computedStyle.getPropertyValue("padding-bottom"));
+ var borderLeft = parseInt(computedStyle.getPropertyValue("border-left-width"));
+ var borderRight = parseInt(computedStyle.getPropertyValue("border-right-width"));
+ var borderTop = parseInt(computedStyle.getPropertyValue("border-top-width"));
+ var borderBottom = parseInt(computedStyle.getPropertyValue("border-bottom-width"));
+
+ //If box-sizing is border-box, we need to subtract padding and border.
+ var parentBoxSizing = computedStyle.getPropertyValue("box-sizing");
+ if (parentBoxSizing == "border-box") {
+ innerWidth = width - (paddingLeft + paddingRight + borderLeft + borderRight);
+ innerHeight = height - (paddingTop + paddingBottom + borderTop + borderBottom);
+ } else {
+ innerWidth = width;
+ innerHeight = height;
+ }
+ var obj = {};
+ obj["width"] = innerWidth;
+ obj["height"] = innerHeight;
+ return obj;
+}
+
+export default function BigText(element, options) {
+
+ if (typeof element === 'string') {
+ element = document.querySelector(element);
+ } else if (element.length) {
+ // Support for array based queries (such as jQuery)
+ element = element[0];
+ }
+
+ var defaultOptions = {
+ rotateText: null,
+ fontSizeFactor: 0.8,
+ maximumFontSize: null,
+ limitingDimension: "both",
+ horizontalAlign: "center",
+ verticalAlign: "center",
+ textAlign: "center",
+ whiteSpace: "nowrap"
+ };
+
+ //Merge provided options and default options
+ options = options || {};
+ for (var opt in defaultOptions)
+ if (defaultOptions.hasOwnProperty(opt) && !options.hasOwnProperty(opt))
+ options[opt] = defaultOptions[opt];
+
+ //Get variables which we will reference frequently
+ var style = element.style;
+ var computedStyle = document.defaultView.getComputedStyle(element);
+ var parent = element.parentNode;
+ var parentStyle = parent.style;
+ var parentComputedStyle = document.defaultView.getComputedStyle(parent);
+
+ //hides the element to prevent "flashing"
+ style.visibility = "hidden";
+
+ //Set some properties
+ style.display = "inline-block";
+ style.clear = "both";
+ style.float = "left";
+ style.fontSize = (1000 * options.fontSizeFactor) + "px";
+ style.lineHeight = "1000px";
+ style.whiteSpace = options.whiteSpace;
+ style.textAlign = options.textAlign;
+ style.position = "relative";
+ style.padding = 0;
+ style.margin = 0;
+ style.left = "50%";
+ style.top = "50%";
+
+ //Get properties of parent to allow easier referencing later.
+ var parentPadding = {
+ top: parseInt(parentComputedStyle.getPropertyValue("padding-top")),
+ right: parseInt(parentComputedStyle.getPropertyValue("padding-right")),
+ bottom: parseInt(parentComputedStyle.getPropertyValue("padding-bottom")),
+ left: parseInt(parentComputedStyle.getPropertyValue("padding-left")),
+ };
+ var parentBorder = {
+ top: parseInt(parentComputedStyle.getPropertyValue("border-top")),
+ right: parseInt(parentComputedStyle.getPropertyValue("border-right")),
+ bottom: parseInt(parentComputedStyle.getPropertyValue("border-bottom")),
+ left: parseInt(parentComputedStyle.getPropertyValue("border-left")),
+ };
+
+ //Calculate the parent inner width and height
+ var parentInnerDimensions = _calculateInnerDimensions(parentComputedStyle);
+ var parentInnerWidth = parentInnerDimensions["width"];
+ var parentInnerHeight = parentInnerDimensions["height"];
+
+ var box = {
+ width: element.offsetWidth, //Note: This is slightly larger than the jQuery version
+ height: element.offsetHeight,
+ };
+
+
+ if (options.rotateText !== null) {
+ if (typeof options.rotateText !== "number")
+ throw "bigText error: rotateText value must be a number";
+ var rotate = "rotate(" + options.rotateText + "deg)";
+ style.webkitTransform = rotate;
+ style.msTransform = rotate;
+ style.MozTransform = rotate;
+ style.OTransform = rotate;
+ style.transform = rotate;
+ //calculating bounding box of the rotated element
+ var sine = Math.abs(Math.sin(options.rotateText * Math.PI / 180));
+ var cosine = Math.abs(Math.cos(options.rotateText * Math.PI / 180));
+ box.width = element.offsetWidth * cosine + element.offsetHeight * sine;
+ box.height = element.offsetWidth * sine + element.offsetHeight * cosine;
+ }
+
+ var widthFactor = (parentInnerWidth - parentPadding.left - parentPadding.right) / box.width;
+ var heightFactor = (parentInnerHeight - parentPadding.top - parentPadding.bottom) / box.height;
+ var lineHeight;
+
+ if (options.limitingDimension.toLowerCase() === "width") {
+ lineHeight = Math.floor(widthFactor * 1000);
+ parentStyle.height = lineHeight + "px";
+ } else if (options.limitingDimension.toLowerCase() === "height") {
+ lineHeight = Math.floor(heightFactor * 1000);
+ } else if (widthFactor < heightFactor)
+ lineHeight = Math.floor(widthFactor * 1000);
+ else if (widthFactor >= heightFactor)
+ lineHeight = Math.floor(heightFactor * 1000);
+
+ var fontSize = lineHeight * options.fontSizeFactor;
+ if (options.maximumFontSize !== null && fontSize > options.maximumFontSize) {
+ fontSize = options.maximumFontSize;
+ lineHeight = fontSize / options.fontSizeFactor;
+ }
+
+ style.fontSize = Math.floor(fontSize) + "px";
+ style.lineHeight = Math.ceil(lineHeight) + "px";
+ style.marginBottom = "0px";
+ style.marginRight = "0px";
+
+ if (options.limitingDimension.toLowerCase() === "height") {
+ //this option needs the font-size to be set already so computedStyle.getPropertyValue("width") returns the right size
+ //this +4 is to compensate the rounding erros that can occur due to the calls to Math.floor in the centering code
+ parentStyle.width = (parseInt(computedStyle.getPropertyValue("width")) + 4) + "px";
+ }
+
+ //Calculate the inner width and height
+ var innerDimensions = _calculateInnerDimensions(computedStyle);
+ var innerWidth = innerDimensions["width"];
+ var innerHeight = innerDimensions["height"];
+
+ switch (options.verticalAlign.toLowerCase()) {
+ case "top":
+ style.top = "0%";
+ break;
+ case "bottom":
+ style.top = "100%";
+ style.marginTop = Math.floor(-innerHeight) + "px";
+ break;
+ default:
+ style.marginTop = Math.ceil((-innerHeight / 2)) + "px";
+ break;
+ }
+
+ switch (options.horizontalAlign.toLowerCase()) {
+ case "left":
+ style.left = "0%";
+ break;
+ case "right":
+ style.left = "100%";
+ style.marginLeft = Math.floor(-innerWidth) + "px";
+ break;
+ default:
+ style.marginLeft = Math.ceil((-innerWidth / 2)) + "px";
+ break;
+ }
+
+ //shows the element after the work is done
+ style.visibility = "visible";
+
+ return element;
+}
diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss
index 109a02df4..6a0d651d2 100644
--- a/src/client/views/nodes/LabelBox.scss
+++ b/src/client/views/nodes/LabelBox.scss
@@ -9,10 +9,10 @@
.labelBox-mainButton {
max-width: 100%;
- width: fit-content;
- height: max-content;
+ width: 100%;
+ height: 100%;
border-radius: inherit;
- letter-spacing: 2px;
+ //letter-spacing: 2px; // bcz: doesn't work with LabelBigText
text-transform: uppercase;
overflow: hidden;
display: inline-block;
@@ -28,5 +28,5 @@
.labelBox-missingParam {
width: 100%;
background: lightgray;
- border: dimGray solid 1px;
+ border: dimgray solid 1px;
} \ No newline at end of file
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index db1ae0537..90b9ce55d 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -1,19 +1,20 @@
-import { action, computed, observable, runInAction } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast } from '../../../fields/Doc';
import { documentSchema } from '../../../fields/documentSchemas';
import { List } from '../../../fields/List';
import { createSchema, listSpec, makeInterface } from '../../../fields/Schema';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, StrCast } from '../../../fields/Types';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxBaseComponent } from '../DocComponent';
+import { StyleProp } from '../StyleProvider';
import { FieldView, FieldViewProps } from './FieldView';
+import BigText from './LabelBigText';
import './LabelBox.scss';
-import { StyleProp } from '../StyleProvider';
const LabelSchema = createSchema({});
@@ -37,7 +38,8 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
}
getTitle() {
- return this.props.label || "";
+ return this.props.label ? this.props.label : this.rootDoc["title-custom"] ? StrCast(this.rootDoc.title) :
+ typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
}
protected createDropTarget = (ele: HTMLDivElement) => {
@@ -81,7 +83,7 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
const missingParams = params?.filter(p => !this.paramsDoc[p]);
params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ...
- const label = this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
+ const label = this.getTitle();
return (
<div className="labelBox-outerDiv"
onMouseLeave={action(() => this._mouseOver = false)}
@@ -90,17 +92,28 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}>
<div className="labelBox-mainButton" style={{
backgroundColor: this.hoverColor,
- fontSize: StrCast(this.layoutDoc._fontSize) || "inherit",
+ fontSize: StrCast(this.layoutDoc._fontSize) || Math.min(18, this.props.PanelHeight() / 2),
fontFamily: StrCast(this.layoutDoc._fontFamily) || "inherit",
letterSpacing: StrCast(this.layoutDoc.letterSpacing),
textTransform: StrCast(this.layoutDoc.textTransform) as any,
- paddingLeft: NumCast(this.layoutDoc._xPadding),
- paddingRight: NumCast(this.layoutDoc._xPadding),
- paddingTop: NumCast(this.layoutDoc._yPadding),
- paddingBottom: NumCast(this.layoutDoc._yPadding),
+ width: this.props.PanelWidth(),
+ height: this.props.PanelHeight(),
whiteSpace: this.layoutDoc._singleLine ? "pre" : "pre-wrap"
}} >
- {label.startsWith("#") ? (null) : label}
+ <span ref={r => {
+ if (r) {
+ BigText(r, {
+ rotateText: null,
+ fontSizeFactor: 1,
+ maximumFontSize: null,
+ limitingDimension: "both",
+ horizontalAlign: "center",
+ verticalAlign: "center",
+ textAlign: "center",
+ whiteSpace: "nowrap"
+ });
+ }
+ }}>{label.startsWith("#") ? (null) : label}</span>
</div>
<div className="labelBox-fieldKeyParams" >
{!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)}
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 1e0172d24..a7bfb93eb 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -128,7 +128,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
<div className="linkAnchorBoxBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.rootDoc)}>
<LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => { })} />
{!this._forceOpen ? (null) : <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => this._isOpen = this._editing = this._forceOpen = false)}>
- <FontAwesomeIcon color="dimGray" icon={"times"} size={"sm"} />
+ <FontAwesomeIcon color="dimgray" icon={"times"} size={"sm"} />
</div>}
</div>
);
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 55ea45bb8..b82d16677 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -2,10 +2,10 @@ import React = require("react");
import { observer } from "mobx-react";
import { documentSchema } from "../../../fields/documentSchemas";
import { makeInterface } from "../../../fields/Schema";
-import { returnFalse } from "../../../Utils";
-import { CollectionTreeView } from "../collections/CollectionTreeView";
+import { emptyFunction, returnFalse } from "../../../Utils";
import { ViewBoxBaseComponent } from "../DocComponent";
import { StyleProp } from "../StyleProvider";
+import { ComparisonBox } from "./ComparisonBox";
import { FieldView, FieldViewProps } from './FieldView';
import "./LinkBox.scss";
@@ -20,22 +20,15 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>(
if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true);
return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`}
style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }} >
- <CollectionTreeView {...this.props}
- childDocuments={[this.dataDoc]}
- treeViewOpen={true}
- treeViewExpandedView={"fields"}
- treeViewHideTitle={true}
- treeViewSkipFields={["treeViewExpandedView", "aliases", "_removeDropProperties",
- "treeViewOpen", "aliasNumber", "isPrototype", "creationDate", "author"]}
+ <ComparisonBox {...this.props}
+ fieldKey="anchor"
+ setHeight={emptyFunction}
dontRegisterView={true}
renderDepth={this.props.renderDepth + 1}
- CollectionView={undefined}
- isAnyChildContentActive={returnFalse}
isContentActive={this.isContentActiveFunc}
addDocument={returnFalse}
removeDocument={returnFalse}
- moveDocument={returnFalse}>
- </CollectionTreeView>
+ moveDocument={returnFalse} />
</div>;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index b62a4dd56..a9d33f161 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -1,8 +1,9 @@
import React = require("react");
+import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import "./LinkDescriptionPopup.scss";
-import { observable, action } from "mobx";
+import { Doc } from "../../../fields/Doc";
import { LinkManager } from "../../util/LinkManager";
+import "./LinkDescriptionPopup.scss";
import { TaskCompletionBox } from "./TaskCompletedBox";
@@ -25,7 +26,7 @@ export class LinkDescriptionPopup extends React.Component<{}> {
onDismiss = (add: boolean) => {
LinkDescriptionPopup.descriptionPopup = false;
if (add) {
- LinkManager.currentLink && (LinkManager.currentLink.description = this.description);
+ LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).description = this.description);
}
}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 126a37eb8..424083dac 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -4,7 +4,7 @@ import { action, computed, observable } from 'mobx';
import { observer } from "mobx-react";
import wiki from "wikijs";
import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from "../../../fields/Doc";
-import { NumCast, StrCast } from "../../../fields/Types";
+import { NumCast, StrCast, Cast } from "../../../fields/Types";
import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils";
import { DocServer } from '../../DocServer';
import { Docs, DocUtils } from "../../documents/Documents";
@@ -14,6 +14,7 @@ import { undoBatch } from '../../util/UndoManager';
import { DocumentView, DocumentViewSharedProps } from "./DocumentView";
import './LinkDocPreview.scss';
import React = require("react");
+import { DocumentType } from '../../documents/DocumentTypes';
interface LinkDocPreviewProps {
linkDoc?: Doc;
@@ -85,7 +86,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
this._linkDoc = DocListCast(anchor.links)[0];
this._linkSrc = anchor;
const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
- this._targetDoc = linkTarget?.annotationOn as Doc ?? linkTarget;
+ this._targetDoc = linkTarget?.type === DocumentType.MARKER && linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
this._toolTipText = "";
}
}));
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 972dcc0be..d54b65d92 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -3,9 +3,9 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
-import { Doc, DocListCast, Opt, WidthSym } from "../../../fields/Doc";
+import { Doc, DocListCast, Opt, WidthSym, StrListCast } from "../../../fields/Doc";
import { documentSchema } from '../../../fields/documentSchemas';
-import { makeInterface } from "../../../fields/Schema";
+import { makeInterface, listSpec } from "../../../fields/Schema";
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { PdfField } from "../../../fields/URLField";
import { TraceMobx } from '../../../fields/util';
@@ -25,6 +25,7 @@ import { FieldView, FieldViewProps } from './FieldView';
import { pageSchema } from "./ImageBox";
import "./PDFBox.scss";
import React = require("react");
+import { CurrentUserUtils } from '../../util/CurrentUserUtils';
type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
@@ -96,7 +97,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
!this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
}
- public search = (string: string, fwd: boolean) => this._pdfViewer?.search(string, fwd);
+ public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => {
+ if (!this._searching && !clear) {
+ this._searching = true;
+ setTimeout(() => {
+ this._searchRef.current?.focus();
+ this._searchRef.current?.select();
+ this._searchRef.current?.setRangeText(searchString);
+ });
+ }
+ return this._pdfViewer?.search(searchString, bwd, clear) || false;
+ });
public prevAnnotation = () => this._pdfViewer?.prevAnnotation();
public nextAnnotation = () => this._pdfViewer?.nextAnnotation();
public backPage = () => { this.Document._curPage = (this.Document._curPage || 1) - 1; return true; };
@@ -107,11 +118,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
onKeyDown = action((e: KeyboardEvent) => {
let processed = false;
switch (e.key) {
- case "f": if (e.ctrlKey) {
- setTimeout(() => this._searchRef.current?.focus(), 100);
- this._searching = processed = true;
- }
- break;
case "PageDown": processed = this.forwardPage(); break;
case "PageUp": processed = this.backPage(); break;
}
@@ -186,8 +192,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<div className="pdfBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
<button className="pdfBox-overlayButton" title={searchTitle} />
<input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged}
- onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} />
- <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}>
+ onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey)} />
+ <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}>
<FontAwesomeIcon icon="search" size="sm" />
</button>
<button className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation} >
@@ -198,7 +204,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</button>
</div>
<button className="pdfBox-overlayButton" title={searchTitle}
- onClick={action(() => { this._searching = !this._searching; this.search("mxytzlaf", true); })} >
+ onClick={action(() => { this._searching = !this._searching; this.search("", true, true); })} >
<div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} />
<div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
<FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" />
@@ -246,9 +252,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@observable _showSidebar = false;
@computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
- contentScaling = () => {
- return 1;
- }
+ contentScaling = () => 1;
@computed get renderPdfView() {
TraceMobx();
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index cdd36eb3b..4871599b8 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -14,6 +14,9 @@
height: 100%;
position: relative;
.videoBox-viewer {
+ display:flex;
+ flex-direction: column;
+ height: 100%;
border-radius: inherit;
opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger
}
@@ -26,7 +29,17 @@
.videoBox-stackPanel {
z-index: -1;
width: 100%;
- position: absolute;
+ position: relative;
+ }
+
+ .videoBox-annotationLayer {
+ position: relative;
+ transform-origin: left top;
+ top: 0;
+ width: 100%;
+ pointer-events: none;
+ mix-blend-mode: multiply; // bcz: makes text fuzzy!
+ overflow: hidden;
}
}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 90de3227f..440ccf638 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -30,6 +30,7 @@ import { DragManager } from "../../util/DragManager";
import { DocumentManager } from "../../util/DocumentManager";
import { DocumentType } from "../../documents/DocumentTypes";
import { Tooltip } from "@material-ui/core";
+import { AnchorMenu } from "../pdf/AnchorMenu";
const path = require('path');
type VideoDocument = makeInterface<[typeof documentSchema]>;
@@ -74,7 +75,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
getAnchor = () => {
const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
- return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined) || this.rootDoc;
+ const marquee = AnchorMenu.Instance.GetAnchor?.();
+ return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
}
videoLoad = () => {
@@ -548,7 +550,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
@computed get annotationLayer() {
- return <div className="imageBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
+ return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
}
marqueeDown = (e: React.PointerEvent) => {
@@ -566,10 +568,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.props.select(true);
});
+ @computed get fitWidth() { return this.props.docViewPath?.().lastElement().fitWidth; }
contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
scaling = () => this.props.scaling?.() || 1;
- panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
- panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100;
+ panelWidth = (): number => this.fitWidth ? this.props.PanelWidth() : (Doc.NativeAspect(this.rootDoc) || 1) * this.panelHeight();
+ panelHeight = (): number => this.fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.heightPercent / 100 * this.props.PanelHeight();
screenToLocalTransform = () => {
const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
@@ -583,10 +586,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont}
style={{
pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined,
- borderRadius
+ borderRadius,
+ overflow: this.props.docViewPath?.().lastElement().fitWidth ? "auto" : undefined
}} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
<div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
- <div style={{ position: "absolute", transition: this.transition, width: this.panelWidth(), height: this.panelHeight(), top: 0, left: `${(100 - this.heightPercent) / 2}%` }}>
+ <div style={{
+ position: "absolute", transition: this.transition,
+ width: this.panelWidth(),
+ height: this.panelHeight(),
+ top: 0,
+ left: (this.props.PanelWidth() - this.panelWidth()) / 2
+ }}>
<CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
renderDepth={this.props.renderDepth + 1}
fieldKey={this.annotationKey}
@@ -606,9 +616,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
{this.contentFunc}
</CollectionFreeFormView>
</div>
- {this.uIButtons}
{this.annotationLayer}
- {this.renderTimeline}
{!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
<MarqueeAnnotator
rootDoc={this.rootDoc}
@@ -623,6 +631,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current}
/>}
+ {this.renderTimeline}
+ {this.uIButtons}
</div>
</div >);
}
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 417a17d96..79289abaa 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -6,6 +6,92 @@
position: relative;
display: flex;
+ .webBox-ui {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ pointer-events: none;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+
+ .webBox-overlayButton {
+ border-bottom-left-radius: 50%;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 20px;
+ background: none;
+ padding: 0;
+ position: absolute;
+ pointer-events: all;
+ color: white;
+ bottom: 0;
+ right: 0;
+
+ .webBox-overlayButton-arrow {
+ width: 0;
+ height: 0;
+ border-top: 10px solid transparent;
+ border-bottom: 10px solid transparent;
+ border-right: 15px solid #121721;
+ transition: all 0.5s;
+ }
+
+ .webBox-overlayButton-iconCont {
+ background: #121721;
+ height: 20px;
+ width: 25px;
+ display: flex;
+ position: relative;
+ align-items: center;
+ justify-content: center;
+ border-radius: 3px;
+ pointer-events: all;
+ }
+ }
+
+ .webBox-nextIcon,
+ .webBox-prevIcon {
+ background: #121721;
+ color: white;
+ height: 20px;
+ width: 25px;
+ display: flex;
+ position: relative;
+ align-items: center;
+ justify-content: center;
+ border-radius: 3px;
+ pointer-events: all;
+ padding: 0px;
+ }
+
+ .webBox-overlayButton:hover {
+ background: none;
+ }
+
+
+ .webBox-overlayCont {
+ position: absolute;
+ width: calc(100% - 40px);
+ height: 20px;
+ background: #121721;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden;
+ transition: left .5s;
+ pointer-events: all;
+
+ .webBox-searchBar {
+ width: 70%;
+ font-size: 14px;
+ }
+ }
+ }
+
.webBox-overlayButton-sidebar {
background: #121721;
height: 25px;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 19135b6dd..9956cc36b 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -16,9 +16,11 @@ import { TraceMobx } from "../../../fields/util";
import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
+import { KeyCodes } from "../../util/KeyCodes";
import { Scripting } from "../../util/Scripting";
import { SnappingManager } from "../../util/SnappingManager";
import { undoBatch } from "../../util/UndoManager";
+import { MarqueeOptionsMenu } from "../collections/collectionFreeForm";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
@@ -30,6 +32,8 @@ import { MarqueeAnnotator } from "../MarqueeAnnotator";
import { AnchorMenu } from "../pdf/AnchorMenu";
import { Annotation } from "../pdf/Annotation";
import { SidebarAnnos } from "../SidebarAnnos";
+import { StyleProp } from "../StyleProvider";
+import { DocumentViewProps } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
import { LinkDocPreview } from "./LinkDocPreview";
import "./WebBox.scss";
@@ -52,6 +56,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _keyInput = React.createRef<HTMLInputElement>();
private _initialScroll: Opt<number>;
private _sidebarRef = React.createRef<SidebarAnnos>();
+ private _searchRef = React.createRef<HTMLInputElement>();
+ private _searchString = "";
+ @observable private _webUrl = ""; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't wan the src parameter to also change as that would cause an unnecessary re-render.
+ @observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe. so thisforces the ifrmae to be rebuilt when allowScripts is toggled
+ @observable private _searching: boolean = false;
+ @observable private _showSidebar = false;
@observable private _scrollTimer: any;
@observable private _overlayAnnoInfo: Opt<Doc>;
@observable private _marqueeing: number[] | undefined;
@@ -69,12 +79,29 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
constructor(props: any) {
super(props);
- // if (true) {// his.webField) {
- // Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850);
- // Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850);
- // }
+ Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850);
+ Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850);
+ runInAction(() => this._webUrl = this._url); // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it.
}
+ @action
+ search = (searchString: string, bwd?: boolean, clear: boolean = false) => {
+ if (!this._searching && !clear) {
+ this._searching = true;
+ setTimeout(() => {
+ this._searchRef.current?.focus();
+ this._searchRef.current?.select();
+ this._searchRef.current?.setRangeText(searchString);
+ });
+ }
+ if (clear) {
+ this._iframe?.contentWindow?.getSelection()?.empty();
+ }
+ if (searchString) {
+ (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true);
+ }
+ return true;
+ }
async componentDidMount() {
this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
@@ -130,6 +157,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
componentWillUnmount() {
Object.values(this._disposers).forEach(disposer => disposer?.());
this._iframe?.removeEventListener('wheel', this.iframeWheel, true);
+ this._iframe?.contentDocument?.removeEventListener("pointerup", this.iframeUp);
}
@action
@@ -193,8 +221,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
iframeUp = (e: PointerEvent) => {
+ this.props.docViewPath().lastElement()?.docView?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here.
if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) {
- this._iframe.contentDocument.addEventListener("pointerup", this.iframeUp);
const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!);
const scale = (this.props.scaling?.() || 1) * mainContBounds.scale;
const sel = this._iframe.contentWindow.getSelection();
@@ -203,7 +231,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX,
e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale);
}
- } else AnchorMenu.Instance.fadeOut(true);
+ }
}
@action
iframeDown = (e: PointerEvent) => {
@@ -214,8 +242,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this._marqueeing = [e.clientX * scale + mainContBounds.translateX,
e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale];
- if (word) {
- this._iframe?.contentDocument?.addEventListener("pointerup", this.iframeUp);
+ if (word || (e.target as any || "").className.includes("rangeslider") || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) {
setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it.
} else {
this._iframeClick = this._iframe ?? undefined;
@@ -224,6 +251,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
e.stopPropagation();
e.preventDefault();
}
+
+ // bcz: hack - iframe grabs all events which messes up how we handle contextMenus. So this super naively simulates the event stack to get the specific menu items and the doc view menu items.
+ if (e.button === 2 || (e.button === 0 && e.altKey)) {
+ e.preventDefault();
+ e.stopPropagation();
+ ContextMenu.Instance.closeMenu();
+ ContextMenu.Instance.setIgnoreEvents(true);
+ }
}
getScrollHeight = () => this._scrollHeight;
@@ -237,8 +272,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
iframeLoaded = (e: any) => {
const iframe = this._iframe;
+ let requrlraw = decodeURIComponent(iframe?.contentWindow?.location.href.replace(Utils.prepend("") + "/corsProxy/", "") ?? this._url.toString());
+ if (requrlraw !== this._url.toString()) {
+ if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) {
+ const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g);
+ const newsearch = matches?.lastElement()!;
+ if (matches) {
+ requrlraw = requrlraw.substring(0, requrlraw.indexOf(newsearch));
+ for (let i = 1; i < Array.from(matches)?.length; i++) {
+ requrlraw = requrlraw.replace(matches[i], "");
+ }
+ }
+ requrlraw = requrlraw.replace(/q=[^&]*/, newsearch.substring(1)).replace("search&", "search?").replace("?gbv=1", "");
+ }
+ this.submitURL(requrlraw, undefined, true);
+ }
if (iframe?.contentDocument) {
- iframe?.contentDocument.addEventListener("pointerdown", this.iframeDown);
+ iframe.contentDocument.addEventListener("pointerup", this.iframeUp);
+ iframe.contentDocument.addEventListener("pointerdown", this.iframeDown);
this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight);
setTimeout(action(() => this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0)), 5000);
const initialScroll = this._initialScroll;
@@ -248,13 +299,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this._initialScroll = undefined;
}
iframe.setAttribute("enable-annotation", "true");
- iframe.contentDocument.addEventListener("click", undoBatch(action(e => {
+ iframe.contentDocument.addEventListener("click", undoBatch(action((e: MouseEvent) => {
let href = "";
- for (let ele = e.target; ele; ele = ele.parentElement) {
+ for (let ele = e.target as any; ele; ele = ele.parentElement) {
href = (typeof (ele.href) === "string" ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href;
}
- if (href && this.webField?.origin) {
- this.submitURL(href.replace(Utils.prepend(""), this.webField?.origin));
+ const origin = this.webField?.origin;
+ if (href && origin) {
+ e.stopPropagation();
+ setTimeout(() => this.submitURL(href.replace(Utils.prepend(""), origin)));
if (this._outerRef.current) {
this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop);
this._outerRef.current.scrollLeft = 0;
@@ -305,8 +358,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), []);
const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []);
if (future.length) {
+ const curUrl = this._url;
this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, this._url]);
this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!));
+ if (this._webUrl === this._url) {
+ this._webUrl = curUrl;
+ setTimeout(action(() => this._webUrl = this._url));
+ } else {
+ this._webUrl = this._url;
+ }
return true;
}
return false;
@@ -317,10 +377,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"));
const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []);
if (history.length) {
+ const curUrl = this._url;
if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]);
else this.dataDoc[this.fieldKey + "-future"] = new List<string>([...future, this._url]);
this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!));
- console.log(this._urlHash);
+ if (this._webUrl === this._url) {
+ this._webUrl = curUrl;
+ setTimeout(action(() => this._webUrl = this._url));
+ } else {
+ this._webUrl = this._url;
+ }
return true;
}
return false;
@@ -329,9 +395,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
static urlHash = (s: string) => {
return Math.abs(s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0));
}
-
@action
- submitURL = (newUrl?: string, preview?: boolean) => {
+ submitURL = (newUrl?: string, preview?: boolean, dontUpdateIframe?: boolean) => {
if (!newUrl) return;
if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl;
try {
@@ -343,7 +408,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.layoutDoc._scrollTop = 0;
future && (future.length = 0);
}
- if (!preview) this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl));
+ if (!preview) {
+ this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl));
+ !dontUpdateIframe && (this._webUrl = this._url);
+ }
} catch (e) {
console.log("WebBox URL error:" + this._url);
}
@@ -391,20 +459,31 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
- specificContextMenu = (e: React.MouseEvent): void => {
+ specificContextMenu = (e: React.MouseEvent | PointerEvent): void => {
const cm = ContextMenu.Instance;
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" });
- funcs.push({
- description: (!this.layoutDoc.allowReflow ? "Allow" : "Prevent") + " Reflow", event: () => {
- const nw = !this.layoutDoc.allowReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.scaling?.() || 1);
- this.layoutDoc.allowReflow = !nw;
- if (nw) {
- Doc.SetInPlace(this.layoutDoc, this.fieldKey + "-nativeWidth", nw, true);
- }
- }, icon: "snowflake"
- });
- cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
+ if (!cm.findByDescription("Options...")) {
+ !Doc.UserDoc().noviceMode && funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" });
+ funcs.push({
+ description: (this.layoutDoc.allowScripts ? "Prevent" : "Allow") + " Scripts", event: () => {
+ this.layoutDoc.allowScripts = !this.layoutDoc.allowScripts;
+ if (this._iframe) {
+ runInAction(() => this._hackHide = true);
+ setTimeout(action(() => this._hackHide = false));
+ }
+ }, icon: "snowflake"
+ });
+ funcs.push({
+ description: (!this.layoutDoc.forceReflow ? "Force" : "Prevent") + " Reflow", event: () => {
+ const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.scaling?.() || 1);
+ this.layoutDoc.forceReflow = !nw;
+ if (nw) {
+ Doc.SetInPlace(this.layoutDoc, this.fieldKey + "-nativeWidth", nw, true);
+ }
+ }, icon: "snowflake"
+ });
+ cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
+ }
}
@action
@@ -417,26 +496,35 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
}
}
- @action finishMarquee = (x?: number, y?: number) => {
+ @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => {
this._marqueeing = undefined;
this._isAnnotating = false;
this._iframeClick = undefined;
- x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false);
+ if (x !== undefined && y !== undefined) {
+ this._setPreviewCursor?.(x, y, false, false);
+ ContextMenu.Instance.closeMenu();
+ ContextMenu.Instance.setIgnoreEvents(false);
+ if (e?.button === 2 || e?.altKey) {
+ this.specificContextMenu(undefined as any);
+ this.props.docViewPath().lastElement().docView?.onContextMenu(undefined, x, y);
+ }
+ }
}
@computed get urlContent() {
+ if (this._hackHide) return (null);
const field = this.dataDoc[this.props.fieldKey];
let view;
if (field instanceof HtmlField) {
view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
} else if (field instanceof WebField) {
- const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._url) : this._url;
+ const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._webUrl) : this._webUrl;
view = <iframe className="webBox-iframe" enable-annotation={"true"}
style={{ pointerEvents: this._scrollTimer ? "none" : undefined }}
ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded}
// the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page
// sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />;
- sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin"} />;
+ sandbox={`${this.layoutDoc.allowScripts ? "allow-scripts" : ""} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`} />;
} else {
view = <iframe className="webBox-iframe" enable-annotation={"true"}
style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly
@@ -491,34 +579,63 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
NumCast(this.layoutDoc.nativeWidth)
@computed get content() {
- return <div className={"webBox-cont" + (!this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")}
- style={{ width: !this.layoutDoc.allowReflow ? NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) || `100%` : "100%", }}>
+ const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents !== "none" && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting;
+ return <div className={"webBox-cont" + (interactive ? "-interactive" : "")}
+ style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) || `100%` : "100%", }}>
{this.urlContent}
</div>;
}
@computed get annotationLayer() {
TraceMobx();
+ const pe = this.pointerEvents();
return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
{this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno =>
- <Annotation {...this.props} fieldKey={this.annotationKey} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)
- }
+ <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={pe} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)}
</div>;
}
- @observable _showSidebar = false;
@computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
+ @computed get searchUI() {
+ return <div className="webBox-ui"
+ onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? "flex" : "none" }}>
+ <div className="webBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
+ <button className="webBox-overlayButton" title={"search"} />
+ <input className="webBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged}
+ onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey)} />
+ <button className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}>
+ <FontAwesomeIcon icon="search" size="sm" />
+ </button>
+ </div>
+ <button className="webBox-overlayButton" title={"search"}
+ onClick={action(() => { this._searching = !this._searching; this.search("", false, true); })} >
+ <div className="webBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} />
+ <div className="webBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
+ <FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" />
+ </div>
+ </button>
+ </div>;
+ }
+ searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;
showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno);
setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => this._setPreviewCursor = func;
panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
+ basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter("textInlineAnnotations")];
transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+ childStyleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => {
+ if (doc instanceof Doc && property === StyleProp.PointerEvents) {
+ if (doc.textInlineAnnotations) return "none";
+ }
+ return this.props.styleProvider?.(doc, props, property);
+ }
+ pointerEvents = () => this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none";
render() {
- const pointerEvents = this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined;
+ const pointerEvents = this.props.layerProvider?.(this.layoutDoc) === false ? "none" : this.props.pointerEvents ? this.props.pointerEvents as any : undefined;
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
const scale = previewScale * (this.props.scaling?.() || 1);
const renderAnnotations = (docFilters?: () => string[]) =>
@@ -533,7 +650,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ScreenToLocalTransform={this.scrollXf}
scaling={returnOne}
dropAction={"alias"}
- docFilters={docFilters || this.props.docFilters}
+ docFilters={docFilters || this.basicFilter}
dontRenderDocuments={docFilters ? false : true}
select={emptyFunction}
ContentScaling={returnOne}
@@ -542,12 +659,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
addDocument={this.sidebarAddDocument}
- childPointerEvents={true}
- pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
+ styleProvider={this.childStyleProvider}
+ childPointerEvents={this.props.isContentActive() ? "all" : undefined}
+ pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
return (
- <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.props.isContentActive() ? "all" : this.props.isContentActive() || SnappingManager.GetIsDragging() ? undefined : "none" }} >
+ <div className="webBox" ref={this._mainCont}
+ style={{ pointerEvents: this.pointerEvents() }} >
<div className={`webBox-container`} style={{ pointerEvents }} onContextMenu={this.specificContextMenu}>
- <base target="_blank" />
<div className={"webBox-outerContent"} ref={this._outerRef}
style={{
width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`,
@@ -605,6 +723,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
onPointerDown={this.sidebarBtnDown} >
<FontAwesomeIcon style={{ color: Colors.WHITE }} icon={"comment-alt"} size="sm" />
</div>
+ {!this.props.isContentActive() ? (null) : this.searchUI}
</div>);
}
}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 9d1ef937f..33fa23805 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -5,7 +5,7 @@ import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ColorState, SketchPicker } from 'react-color';
-import { Doc, StrListCast } from '../../../../fields/Doc';
+import { Doc, StrListCast, WidthSym, HeightSym } from '../../../../fields/Doc';
import { InkTool } from '../../../../fields/InkField';
import { createSchema, makeInterface } from '../../../../fields/Schema';
import { ScriptField } from '../../../../fields/ScriptField';
@@ -14,7 +14,6 @@ import { WebField } from '../../../../fields/URLField';
import { DocumentType } from '../../../documents/DocumentTypes';
import { Scripting } from "../../../util/Scripting";
import { SelectionManager } from '../../../util/SelectionManager';
-import { ColorScheme } from '../../../util/SettingsManager';
import { UndoManager, undoBatch } from '../../../util/UndoManager';
import { CollectionViewType } from '../../collections/CollectionView';
import { ContextMenu } from '../../ContextMenu';
@@ -114,7 +113,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon
// Script for checking the outcome of the toggle
const checkScript: string = StrCast(this.rootDoc.script) + "(0, true)";
- const checkResult: number = ScriptField.MakeScript(checkScript)?.script.run().result;
+ const checkResult: number = ScriptField.MakeScript(checkScript)?.script.run().result || 0;
if (numBtnType === NumButtonType.Slider) {
@@ -159,7 +158,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon
<div
className={`menuButton ${this.type} ${numBtnType}`}
>
- <div className={`button`} onClick={action((e) => setValue(checkResult - 1))}>
+ <div className={`button`} onClick={action((e) => setValue(Number(checkResult) - 1))}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={"minus"} />
</div>
<div
@@ -178,7 +177,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon
onChange={action((e) => setValue(Number(e.target.value)))}
/>
</div>
- <div className={`button`} onClick={action((e) => setValue(checkResult + 1))}>
+ <div className={`button`} onClick={action((e) => setValue(Number(checkResult) + 1))}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={"plus"} />
</div>
@@ -262,8 +261,8 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon
}
noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking];
} else if (script === 'setFont') {
- const selected = SelectionManager.Docs().lastElement();
- text = StrCast((selected?.type === DocumentType.RTF ? selected : Doc.UserDoc())._fontFamily);
+ const editorView = RichTextMenu.Instance?.TextView?.EditorView;
+ text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
noviceList = ["Roboto", "Times New Roman", "Arial", "Georgia",
"Comic Sans MS", "Tahoma", "Impact", "Crimson Text"];
}
@@ -319,29 +318,35 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon
);
}
+ @observable colorPickerClosed: boolean = true;
+ @computed get colorScript() {
+ const script = StrCast(this.rootDoc.script);
+ return ScriptField.MakeScript(script + '(colValue, checkResult)', { colValue: "string", checkResult: "boolean" });
+ }
+
+ colorPicker = (curColor: string) => {
+ const change = (value: ColorState) => {
+ const s = this.colorScript;
+ s && undoBatch(() => s.script.run({ colValue: Utils.colorString(value), checkResult: false }).result)();
+ };
+ const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505',
+ '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B',
+ '#FFFFFF', '#f1efeb', "transparent"];
+ return <SketchPicker
+ onChange={change}
+ color={curColor}
+ presetColors={presets} />;
+ }
/**
* Color button
*/
@computed get colorButton() {
- const active: string = StrCast(this.rootDoc.dropDownOpen);
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+ const curColor = this.colorScript?.script.run({ colValue: undefined, checkResult: true }).result ?? "transparent";
- const script: string = StrCast(this.rootDoc.script);
- const scriptCheck: string = script + "(undefined, true)";
- const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result;
-
- const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505',
- '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B',
- '#FFFFFF', '#f1efeb', "transparent"];
-
- const colorBox = (func: (color: ColorState) => void) => <SketchPicker
- disableAlpha={false}
- onChange={func}
- color={boolResult ? boolResult : "#FFFFFF"}
- presetColors={colorOptions} />;
const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
- <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}>
+ <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}>
{this.label}
</div>;
@@ -350,30 +355,27 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon
style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
<FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
</div>;
-
- const click = (value: ColorState) => {
- const s = ScriptField.MakeScript(script + '("' + Utils.colorString(value) + '", false)');
- s && undoBatch(() => s.script.run().result)();
- };
+ setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView
return (
- <div className={`menuButton ${this.type} ${active}`}
+ <div className={`menuButton ${this.type} ${this.colorPickerClosed}`}
style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
- onClick={() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen}
+ onClick={action(() => this.colorPickerClosed = !this.colorPickerClosed)}
onPointerDown={e => e.stopPropagation()}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
- <div className="colorButton-color" style={{ backgroundColor: boolResult ? boolResult : "#FFFFFF" }} />
+ <div className="colorButton-color" style={{ backgroundColor: curColor }} />
{label}
{/* {dropdownCaret} */}
- {this.rootDoc.dropDownOpen ?
+ {this.colorPickerClosed ? (null) :
<div>
<div className="menuButton-dropdownBox"
onPointerDown={e => e.stopPropagation()}
onClick={e => e.stopPropagation()}>
- {colorBox(click)}
+ {this.colorPicker(curColor)}
</div>
- <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; }} />
- </div>
- : null}
+ <div className="dropbox-background" onClick={action((e) => {
+ e.stopPropagation(); this.colorPickerClosed = true;
+ })} />
+ </div>}
</div>
);
}
@@ -388,7 +390,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon
// Button label
const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
- <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}>
+ <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}>
{this.label}
</div>;
@@ -407,7 +409,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon
} else {
return (
<div className={`menuButton ${this.type}`}
- style={{ opacity: 1, backgroundColor: backgroundColor, color: color }}>
+ style={{ opacity: 1, backgroundColor, color }}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
{label}
</div>
@@ -463,7 +465,6 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon
render() {
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const dark: boolean = Doc.UserDoc().colorScheme === ColorScheme.Dark;
const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
<div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}>
@@ -573,15 +574,9 @@ Scripting.addGlobal(function setView(view: string) {
Scripting.addGlobal(function setBackgroundColor(color?: string, checkResult?: boolean) {
const selected = SelectionManager.Docs().lastElement();
if (checkResult) {
- if (selected) {
- return selected._backgroundColor;
- } else {
- return "#FFFFFF";
- }
+ return selected?._backgroundColor ?? "transparent";
}
- if (selected?.type === DocumentType.INK) selected.fillColor = color;
if (selected) selected._backgroundColor = color;
- Doc.UserDoc()._fontColor = color;
});
// toggle: Set overlay status of selected document
@@ -599,7 +594,7 @@ Scripting.addGlobal(function toggleOverlay(checkResult?: boolean) {
const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
if (checkResult && selected) {
if (NumCast(selected.Document.z) >= 1) return Colors.MEDIUM_BLUE;
- else return "transparent";
+ return "transparent";
}
selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("[FontIconBox.tsx] toggleOverlay failed");
});
@@ -620,16 +615,18 @@ Scripting.addGlobal(function toggleOverlay(checkResult?: boolean) {
Scripting.addGlobal(function setFont(font: string, checkResult?: boolean) {
SelectionManager.Docs().map(doc => doc._fontFamily = font);
const editorView = RichTextMenu.Instance.TextView?.EditorView;
- editorView?.state && RichTextMenu.Instance.setFontFamily(font, editorView);
- Doc.UserDoc()._fontFamily = font;
- return Doc.UserDoc()._fontFamily;
+ if (checkResult) {
+ return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
+ }
+ if (editorView) RichTextMenu.Instance.setFontFamily(font);
+ else Doc.UserDoc().fontFamily = font;
});
Scripting.addGlobal(function getActiveTextInfo(info: "family" | "size" | "color" | "highlight") {
const editorView = RichTextMenu.Instance.TextView?.EditorView;
const style = editorView?.state && RichTextMenu.Instance.getActiveFontStylesOnSelection();
switch (info) {
- case "family": return style?.activeColors[0];
+ case "family": return style?.activeFamilies[0];
case "size": return style?.activeSizes[0];
case "color": return style?.activeColors[0];
case "highlight": return style?.activeHighlights[0];
@@ -646,7 +643,7 @@ Scripting.addGlobal(function setAlignment(align: "left" | "right" | "center", ch
active = StrCast(Doc.UserDoc().textAlign);
}
if (active === align) return Colors.MEDIUM_BLUE;
- else return "transparent";
+ return "transparent";
}
SelectionManager.Docs().map(doc => doc.textAlign = align);
switch (align) {
@@ -670,38 +667,25 @@ Scripting.addGlobal(function setBulletList(mapStyle: "bullet" | "decimal", check
if (checkResult) {
const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle();
if (active === mapStyle) return Colors.MEDIUM_BLUE;
- else return "transparent";
+ return "transparent";
}
if (editorView) {
const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle();
- if (active === mapStyle) {
- editorView?.state && RichTextMenu.Instance.changeListType(editorView.state.schema.nodes.ordered_list.create({ mapStyle: "" }));
- } else {
- editorView?.state && RichTextMenu.Instance.changeListType(editorView.state.schema.nodes.ordered_list.create({ mapStyle: mapStyle }));
- }
+ editorView?.state && RichTextMenu.Instance.changeListType(
+ editorView.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? "" : mapStyle }));
}
});
// toggle: Set overlay status of selected document
Scripting.addGlobal(function setFontColor(color?: string, checkResult?: boolean) {
- const selected = SelectionManager.Docs().lastElement();
const editorView = RichTextMenu.Instance.TextView?.EditorView;
if (checkResult) {
- if (selected) {
- return selected._fontColor;
- } else {
- return Doc.UserDoc()._fontColor;
- }
+ return editorView ? RichTextMenu.Instance.fontColor : Doc.UserDoc().fontColor;
}
- if (selected) {
- selected._fontColor = color;
- if (color) {
- editorView?.state && RichTextMenu.Instance.setColor(color, editorView, editorView?.dispatch);
- }
- }
- Doc.UserDoc()._fontColor = color;
+ if (editorView) color && RichTextMenu.Instance.setColor(color, editorView, editorView?.dispatch);
+ else Doc.UserDoc().fontColor = color;
});
// toggle: Set overlay status of selected document
@@ -710,11 +694,7 @@ Scripting.addGlobal(function setFontHighlight(color?: string, checkResult?: bool
const editorView = RichTextMenu.Instance.TextView?.EditorView;
if (checkResult) {
- if (selected) {
- return selected._fontHighlight;
- } else {
- return Doc.UserDoc()._fontHighlight;
- }
+ return (selected ?? Doc.UserDoc())._fontHighlight;
}
if (selected) {
selected._fontColor = color;
@@ -725,62 +705,43 @@ Scripting.addGlobal(function setFontHighlight(color?: string, checkResult?: bool
Doc.UserDoc()._fontHighlight = color;
});
-
-
// toggle: Set overlay status of selected document
-Scripting.addGlobal(function setFontSize(size: string, checkResult?: boolean) {
+Scripting.addGlobal(function setFontSize(size: string | number, checkResult?: boolean) {
+ if (typeof size === "number") size = size.toString();
+ if (size && Number(size).toString() === size) size += "px";
+ const editorView = RichTextMenu.Instance.TextView?.EditorView;
if (checkResult) {
- const size: number = parseInt(StrCast(Doc.UserDoc()._fontSize), 10);
- return size;
+ return (editorView ? RichTextMenu.Instance.fontSize : StrCast(Doc.UserDoc().fontSize, "10px")).replace("px", "");
}
- const editorView = RichTextMenu.Instance.TextView?.EditorView;
- editorView?.state && RichTextMenu.Instance.setFontSize(Number(size), editorView);
- Doc.UserDoc()._fontSize = size + "px";
+ if (editorView) RichTextMenu.Instance.setFontSize(size);
+ else Doc.UserDoc()._fontSize = size;
});
Scripting.addGlobal(function toggleBold(checkResult?: boolean) {
+ const editorView = RichTextMenu.Instance?.TextView?.EditorView;
if (checkResult) {
- const editorView = RichTextMenu.Instance.TextView?.EditorView;
- if (editorView) {
- const bold: boolean = editorView?.state && RichTextMenu.Instance.getBoldStatus();
- if (bold) return Colors.MEDIUM_BLUE;
- else return "transparent";
- }
- else return "transparent";
+ return (editorView ? RichTextMenu.Instance.bold : Doc.UserDoc().fontWeight === "bold") ? Colors.MEDIUM_BLUE : "transparent";
}
- const editorView = RichTextMenu.Instance.TextView?.EditorView;
- if (editorView) {
- editorView?.state && RichTextMenu.Instance.toggleBold(editorView, true);
- }
- SelectionManager.Docs().filter(doc => StrCast(doc.type) === DocumentType.RTF).map(doc => doc.bold = !doc.bold);
- Doc.UserDoc().bold = !Doc.UserDoc().bold;
- return Doc.UserDoc().bold;
+ if (editorView) RichTextMenu.Instance?.toggleBold();
+ else Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === "bold" ? undefined : "bold";
});
Scripting.addGlobal(function toggleUnderline(checkResult?: boolean) {
+ const editorView = RichTextMenu.Instance?.TextView?.EditorView;
if (checkResult) {
- return "transparent";
+ return (editorView ? RichTextMenu.Instance.underline : Doc.UserDoc().textDecoration === "underline") ? Colors.MEDIUM_BLUE : "transparent";
}
- const editorView = RichTextMenu.Instance.TextView?.EditorView;
- if (editorView) {
- editorView?.state && RichTextMenu.Instance.toggleUnderline(editorView, true);
- }
- SelectionManager.Docs().filter(doc => StrCast(doc.type) === DocumentType.RTF).map(doc => doc.underline = !doc.underline);
- Doc.UserDoc().underline = !Doc.UserDoc().underline;
- return Doc.UserDoc().underline;
+ if (editorView) RichTextMenu.Instance?.toggleUnderline();
+ else Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === "underline" ? undefined : "underline";
});
Scripting.addGlobal(function toggleItalic(checkResult?: boolean) {
+ const editorView = RichTextMenu.Instance?.TextView?.EditorView;
if (checkResult) {
- return "transparent";
+ return (editorView ? RichTextMenu.Instance.italics : Doc.UserDoc().fontStyle === "italics") ? Colors.MEDIUM_BLUE : "transparent";
}
- const editorView = RichTextMenu.Instance.TextView?.EditorView;
- if (editorView) {
- editorView?.state && RichTextMenu.Instance.toggleItalic(editorView, true);
- }
- SelectionManager.Docs().filter(doc => StrCast(doc.type) === DocumentType.RTF).map(doc => doc.italic = !doc.italic);
- Doc.UserDoc().italic = !Doc.UserDoc().italic;
- return Doc.UserDoc().italic;
+ if (editorView) RichTextMenu.Instance?.toggleItalics();
+ else Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === "italics" ? undefined : "italics";
});
@@ -794,24 +755,23 @@ Scripting.addGlobal(function toggleItalic(checkResult?: boolean) {
Scripting.addGlobal(function setActiveInkTool(tool: string, checkResult?: boolean) {
if (checkResult) {
- if (Doc.UserDoc().activeInkTool === tool && GestureOverlay.Instance.InkShape === "" || GestureOverlay.Instance.InkShape === tool) return Colors.MEDIUM_BLUE;
- else return "transparent";
+ return ((Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool) ?
+ Colors.MEDIUM_BLUE : "transparent";
}
- if (tool === "circle") {
- Doc.UserDoc().activeInkTool = "pen";
- GestureOverlay.Instance.InkShape = tool;
- } else if (tool === "square") {
- Doc.UserDoc().activeInkTool = "pen";
- GestureOverlay.Instance.InkShape = tool;
- } else if (tool === "line") {
- Doc.UserDoc().activeInkTool = "pen";
- GestureOverlay.Instance.InkShape = tool;
- } else if (tool) {
- if (Doc.UserDoc().activeInkTool === tool && GestureOverlay.Instance.InkShape === "" || GestureOverlay.Instance.InkShape === tool) {
- GestureOverlay.Instance.InkShape = "";
+ if (["circle", "square", "line"].includes(tool)) {
+ if (GestureOverlay.Instance.InkShape === tool) {
+ Doc.UserDoc().activeInkTool = InkTool.None;
+ GestureOverlay.Instance.InkShape = InkTool.None;
+ } else {
+ Doc.UserDoc().activeInkTool = InkTool.Pen;
+ GestureOverlay.Instance.InkShape = tool;
+ }
+ } else if (tool) { // pen
+ if (Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance.InkShape) {
Doc.UserDoc().activeInkTool = InkTool.None;
} else {
Doc.UserDoc().activeInkTool = tool;
+ GestureOverlay.Instance.InkShape = "";
}
} else {
Doc.UserDoc().activeInkTool = InkTool.None;
@@ -823,7 +783,7 @@ Scripting.addGlobal(function setFillColor(color?: string, checkResult?: boolean)
const selected = SelectionManager.Docs().lastElement();
if (checkResult) {
if (selected?.type === DocumentType.INK) {
- return StrCast(selected._backgroundColor);
+ return StrCast(selected.fillColor);
}
return ActiveFillColor();
}
@@ -901,4 +861,17 @@ Scripting.addGlobal(function toggleSchemaPreview(checkResult?: boolean) {
selected.schemaPreviewWidth = 0;
}
}
+});
+
+/** STACK
+ * groupBy
+ */
+Scripting.addGlobal(function setGroupBy(key: string, checkResult?: boolean) {
+ SelectionManager.Docs().map(doc => doc._fontFamily = key);
+ const editorView = RichTextMenu.Instance.TextView?.EditorView;
+ if (checkResult) {
+ return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
+ }
+ if (editorView) RichTextMenu.Instance.setFontFamily(key);
+ else Doc.UserDoc().fontFamily = key;
}); \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 8915d7c47..364be461f 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -1,7 +1,7 @@
import { IReactionDisposer, reaction, observable, action } from "mobx";
import { NodeSelection } from "prosemirror-state";
import { Doc, HeightSym, WidthSym } from "../../../../fields/Doc";
-import { Cast, StrCast } from "../../../../fields/Types";
+import { Cast, StrCast, NumCast } from "../../../../fields/Types";
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, Utils, returnTransparent } from "../../../../Utils";
import { DocServer } from "../../../DocServer";
import { Docs, DocUtils } from "../../../documents/Documents";
@@ -12,6 +12,7 @@ import { FormattedTextBox } from "./FormattedTextBox";
import React = require("react");
import * as ReactDOM from 'react-dom';
import { observer } from "mobx-react";
+import { ColorScheme } from "../../../util/SettingsManager";
export class DashDocView {
_fieldWrapper: HTMLSpanElement; // container for label and value
@@ -20,7 +21,7 @@ export class DashDocView {
this._fieldWrapper = document.createElement("span");
this._fieldWrapper.style.position = "relative";
this._fieldWrapper.style.textIndent = "0";
- this._fieldWrapper.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (CurrentUserUtils.ActiveDashboard.darkScheme ? "dimGray" : "lightGray"));
+ this._fieldWrapper.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "dimgray" : "lightGray"));
this._fieldWrapper.style.width = node.attrs.width;
this._fieldWrapper.style.height = node.attrs.height;
this._fieldWrapper.style.display = node.attrs.hidden ? "none" : "inline-block";
@@ -69,30 +70,31 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
@observable _finalLayout: any;
@observable _resolvedDataDoc: any;
- constructor(props: IDashDocViewInternal) {
- super(props);
- this._textBox = this.props.tbox;
- const updateDoc = action((dashDoc: Doc) => {
- this._dashDoc = dashDoc;
- this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey);
+ updateDoc = action((dashDoc: Doc) => {
+ this._dashDoc = dashDoc;
+ this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey);
- if (this._finalLayout) {
- if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) {
- this._finalLayout.rootDocument = dashDoc.aliasOf;
- }
- this._resolvedDataDoc = Cast(this._finalLayout.resolvedDataDoc, Doc, null);
+ if (this._finalLayout) {
+ if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) {
+ this._finalLayout.rootDocument = dashDoc.aliasOf;
}
- if (this.props.width !== (this._dashDoc?._width ?? "") + "px" || this.props.height !== (this._dashDoc?._height ?? "") + "px") {
- try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
- this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, {
- ...this.props.node.attrs, width: (this._dashDoc?._width ?? "") + "px", height: (this._dashDoc?._height ?? "") + "px"
- }));
- } catch (e) {
- console.log("DashDocView:" + e);
- }
+ this._resolvedDataDoc = Cast(this._finalLayout.resolvedDataDoc, Doc, null);
+ }
+ if (this.props.width !== (this._dashDoc?._width ?? "") + "px" || this.props.height !== (this._dashDoc?._height ?? "") + "px") {
+ try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, {
+ ...this.props.node.attrs, width: (this._dashDoc?._width ?? "") + "px", height: (this._dashDoc?._height ?? "") + "px"
+ }));
+ } catch (e) {
+ console.log("DashDocView:" + e);
}
- });
+ }
+ });
+
+ constructor(props: IDashDocViewInternal) {
+ super(props);
+ this._textBox = this.props.tbox;
DocServer.GetRefField(this.props.docid + this.props.alias).then(async dashDoc => {
if (!(dashDoc instanceof Doc)) {
@@ -101,15 +103,27 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docid + this.props.alias);
aliasedDoc.layoutKey = "layout";
this.props.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, this.props.fieldKey, undefined);
- updateDoc(aliasedDoc);
+ this.updateDoc(aliasedDoc);
}
});
} else {
- updateDoc(dashDoc);
+ this.updateDoc(dashDoc);
}
});
}
+ componentDidMount() {
+ this._disposers.upater = reaction(() => this._dashDoc && (NumCast(this._dashDoc._height) + NumCast(this._dashDoc._width)),
+ () => {
+ if (this._dashDoc) {
+ this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, {
+ ...this.props.node.attrs, width: (this._dashDoc?._width ?? "") + "px", height: (this._dashDoc?._height ?? "") + "px"
+ }));
+ }
+ });
+ }
+
+
removeDoc = () => {
this.props.view.dispatch(this.props.view.state.tr
.setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.getPos())))
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
index e7dd286a5..c36e6804b 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.scss
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -8,7 +8,7 @@
height: 10px;
position: relative;
display: inline-block;
- background: dimGray;
+ background: dimgray;
}
.dashFieldView-fieldCheck {
min-width: 12px;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 0d38bd5b8..cfbd1962e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -280,8 +280,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
(curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())));
if ((!curTemp && !curProto) || curText || json.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
if (removeSelection(json) !== removeSelection(curLayout?.Data)) {
- !curText && tx.storedMarks?.filter(m => m.type.name === "pFontSize").map(m => Doc.UserDoc().fontSize = this.layoutDoc._fontSize = (m.attrs.fontSize + "px"));
- !curText && tx.storedMarks?.filter(m => m.type.name === "pFontFamily").map(m => Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily);
this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText);
this.dataDoc[this.props.fieldKey + "-noTemplate"] = true;//(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
@@ -463,10 +461,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const target = dragData.droppedDocuments[0];
target._fitToBox = true;
const node = schema.nodes.dashDoc.create({
- width: target[WidthSym](), height: target[HeightSym](),
+ width: target[WidthSym](),
+ height: target[HeightSym](),
title: "dashDoc",
docid: target[Id],
- float: "right"
+ float: "unset"
});
const view = this._editorView!;
view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
@@ -833,7 +832,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and autoHeight is on
() => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }),
({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => {
- autoHeight && this.props.setHeight(marginsHeight + Math.max(sidebarHeight, textHeight));
+ autoHeight && this.props.setHeight?.(marginsHeight + Math.max(sidebarHeight, textHeight));
}, { fireImmediately: true });
this._disposers.links = reaction(() => DocListCast(this.Document.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
newLinks => {
@@ -894,11 +893,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
{ fireImmediately: Doc.IsSearchMatchUnmemoized(this.rootDoc) ? true : false });
this._disposers.selected = reaction(() => this.props.isSelected(),
- action((selected) => {
+ action(selected => {
if (RichTextMenu.Instance?.view === this._editorView && !selected) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined);
}
- }));
+ if (this._editorView && selected) {
+ RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
+ }
+ }), { fireImmediately: true });
if (!this.props.dontRegisterView) {
this._disposers.record = reaction(() => this._recording,
@@ -959,7 +961,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
GoogleApiClientUtils.Docs.write({ reference, content, mode });
}
};
- UndoManager.AddEvent({ undo, redo });
+ UndoManager.AddEvent({ undo, redo, prop: "" });
redo();
});
}
@@ -1168,7 +1170,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
selectOnLoad && this._editorView!.focus();
// 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.
if (this._editorView && !this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark)) {
- this._editorView.state.storedMarks = [...(this._editorView.state.storedMarks ?? []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })];
+ this._editorView.state.storedMarks = [...(this._editorView.state.storedMarks ?? []),
+ schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }),
+ ...(Doc.UserDoc().fontColor !== "transparent" && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
+ ...(Doc.UserDoc().fontStyle === "italics" ? [schema.mark(schema.marks.em)] : []),
+ ...(Doc.UserDoc().textDecoration === "underline" ? [schema.mark(schema.marks.underline)] : []),
+ ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: StrCast(Doc.UserDoc().fontFamily) })] : []),
+ ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: StrCast(Doc.UserDoc().fontSize, "") })] : []),
+ ...(Doc.UserDoc().fontWeight === "bold" ? [schema.mark(schema.marks.strong)] : [])];
}
}
@@ -1187,9 +1196,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if ((e.target as any).tagName === "AUDIOTAG") {
e.preventDefault();
e.stopPropagation();
+ const timecode = Number((e.target as any)?.dataset?.timecode);
DocServer.GetRefField((e.target as any)?.dataset?.audioid || 0).then(anchor => {
if (anchor instanceof Doc) {
- const timecode = NumCast(anchor.timecodeToShow, 0);
+ // const timecode = NumCast(anchor.timecodeToShow, 0);
const audiodoc = anchor.annotationOn as Doc;
const func = () => {
const docView = DocumentManager.Instance.getDocumentView(audiodoc);
@@ -1468,19 +1478,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
tryUpdateScrollHeight = () => {
- if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
- const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
- const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
- if (children) {
- const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
- const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
- if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
- const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
- if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
- setScrollHeight();
- } else {
- setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived...
- }
+ const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
+ const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
+ if (children) {
+ const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
+ const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
+ if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+ const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
+ if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
+ setScrollHeight();
+ } else {
+ setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived...
}
}
}
@@ -1601,7 +1609,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
background: this.props.background ? this.props.background : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
color: this.props.color ? this.props.color : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
fontSize: this.props.fontSize ? this.props.fontSize : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize),
- fontWeight: Cast(this.layoutDoc._fontWeight, "number", null),
+ fontWeight: Cast(this.layoutDoc._fontWeight, "string", null) as any,
fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"),
pointerEvents: interactive ? undefined : "none",
}}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 9904a7939..bd05af977 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -1,8 +1,7 @@
import React = require("react");
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from "@material-ui/core";
-import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { action, IReactionDisposer, observable, reaction, runInAction, computed } from "mobx";
import { observer } from "mobx-react";
import { lift, wrapIn } from "prosemirror-commands";
import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos } from "prosemirror-model";
@@ -10,10 +9,7 @@ import { wrapInList } from "prosemirror-schema-list";
import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Doc } from "../../../../fields/Doc";
-import { DarkPastelSchemaPalette, PastelSchemaPalette } from '../../../../fields/SchemaHeaderField';
import { Cast, StrCast } from "../../../../fields/Types";
-import { TraceMobx } from "../../../../fields/util";
-import { unimplementedFunction, Utils } from "../../../../Utils";
import { DocServer } from "../../../DocServer";
import { LinkManager } from "../../../util/LinkManager";
import { SelectionManager } from "../../../util/SelectionManager";
@@ -29,7 +25,7 @@ const { toggleMark } = require("prosemirror-commands");
@observer
export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
- static Instance: RichTextMenu;
+ @observable static Instance: RichTextMenu;
public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
private _linkToRef = React.createRef<HTMLInputElement>();
@@ -39,22 +35,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
public _brushMap: Map<string, Set<Mark>> = new Map();
@observable private collapsed: boolean = false;
- @observable private boldActive: boolean = false;
- @observable private italicsActive: boolean = false;
- @observable private underlineActive: boolean = false;
- @observable private strikethroughActive: boolean = false;
- @observable private subscriptActive: boolean = false;
- @observable private superscriptActive: boolean = false;
-
- @observable private activeFontSize: string = "";
- @observable private activeFontFamily: string = "";
+ @observable private _boldActive: boolean = false;
+ @observable private _italicsActive: boolean = false;
+ @observable private _underlineActive: boolean = false;
+ @observable private _strikethroughActive: boolean = false;
+ @observable private _subscriptActive: boolean = false;
+ @observable private _superscriptActive: boolean = false;
+
+ @observable private _activeFontSize: string = "13px";
+ @observable private _activeFontFamily: string = "";
@observable private activeListType: string = "";
@observable private activeAlignment: string = "left";
@observable private brushMarks: Set<Mark> = new Set();
@observable private showBrushDropdown: boolean = false;
- @observable private activeFontColor: string = "black";
+ @observable private _activeFontColor: string = "black";
@observable private showColorDropdown: boolean = false;
@observable private activeHighlightColor: string = "transparent";
@@ -67,10 +63,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
_delayHide = false;
constructor(props: Readonly<{}>) {
super(props);
- RichTextMenu.Instance = this;
- this._canFade = false;
- //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]);
- runInAction(() => this.Pinned = true);
+ runInAction(() => {
+ RichTextMenu.Instance = this;
+ this._canFade = false;
+ //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]);
+ this.Pinned = true;
+ });
}
componentDidMount() {
@@ -81,6 +79,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this._reaction?.();
}
+ @computed get bold() { return this._boldActive; }
+ @computed get underline() { return this._underlineActive; }
+ @computed get italics() { return this._italicsActive; }
+ @computed get strikeThrough() { return this._strikethroughActive; }
+ @computed get fontColor() { return this._activeFontColor; }
+ @computed get fontFamily() { return this._activeFontFamily; }
+ @computed get fontSize() { return this._activeFontSize; }
+
public delayHide = () => this._delayHide = true;
@action
@@ -110,10 +116,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.activeListType = this.getActiveListStyle();
this.activeAlignment = this.getActiveAlignment();
- this.activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
- this.activeFontSize = !activeSizes.length ? "13px" : activeSizes.length === 1 ? String(activeSizes[0]) : "...";
- this.activeFontColor = !activeColors.length ? "black" : activeColors.length === 1 ? String(activeColors[0]) : "...";
- this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length === 1 ? String(activeHighlights[0]) : "...";
+ this._activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
+ this._activeFontSize = !activeSizes.length ? "13px" : activeSizes[0];
+ this._activeFontColor = !activeColors.length ? "black" : activeColors.length > 0 ? String(activeColors[0]) : "...";
+ this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length > 0 ? String(activeHighlights[0]) : "...";
// update link in current selection
this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle));
@@ -125,7 +131,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
if (node?.type === schema.nodes.ordered_list) {
let attrs = node.attrs;
if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family };
- if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: `${mark.attrs.fontSize}px` };
+ if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: mark.attrs.fontSize };
if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color };
const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
@@ -142,17 +148,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
}
- getBoldStatus() {
- if (this.view && this.TextView.props.isSelected(true)) {
- const path = (this.view.state.selection.$from as any).path;
- for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) {
- if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) {
- return path[i].attrs.strong;
- }
- }
- }
- }
-
// finds font sizes and families in selection
getActiveAlignment() {
if (this.view && this.TextView.props.isSelected(true)) {
@@ -193,25 +188,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
if (this.TextView.props.isSelected(true)) {
const state = this.view.state;
const pos = this.view.state.selection.$from;
- const ref_node = this.reference_node(pos);
- if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
- const marks = Array.from(ref_node.marks);
- marks.push(...(this.view.state.storedMarks as any));
- marks.forEach(m => {
- m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
- m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color);
- m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "px");
- m.type === state.schema.marks.marker && activeHighlights.push(String(m.attrs.highlight));
+ const marks: Mark<any>[] = [...(state.storedMarks ?? [])];
+ if (state.selection.empty) {
+ const ref_node = this.reference_node(pos);
+ marks.push(...(ref_node !== this.view.state.doc && ref_node?.isText ? Array.from(ref_node.marks) : []));
+ } else {
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
+ node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark));
});
}
- !activeFamilies.length && (activeFamilies.push(StrCast(this.TextView.layoutDoc._fontFamily, StrCast(Doc.UserDoc().fontFamily))));
- !activeSizes.length && (activeSizes.push(StrCast(this.TextView.layoutDoc._fontSize, StrCast(Doc.UserDoc().fontSize))));
- !activeColors.length && (activeColors.push(StrCast(this.TextView.layoutDoc.color, StrCast(Doc.UserDoc().fontColor))));
+ marks.forEach(m => {
+ m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
+ m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color);
+ m.type === state.schema.marks.pFontSize && activeSizes.push(m.attrs.fontSize);
+ m.type === state.schema.marks.marker && activeHighlights.push(String(m.attrs.highlight));
+ });
}
- !activeFamilies.length && (activeFamilies.push(StrCast(Doc.UserDoc().fontFamily)));
- !activeSizes.length && (activeSizes.push(StrCast(Doc.UserDoc().fontSize)));
- !activeColors.length && (activeColors.push(StrCast(Doc.UserDoc().fontColor, "black")));
- !activeHighlights.length && (activeHighlights.push(StrCast(Doc.UserDoc().fontHighlight, "")));
return { activeFamilies, activeSizes, activeColors, activeHighlights };
}
@@ -251,11 +243,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return [];
}
activeMarks = markGroup.filter(mark_type => {
- if (mark_type === state.schema.marks.pFontSize) {
- return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
- }
+ // if (mark_type === state.schema.marks.pFontSize) {
+ // return mark.isINSet
+ // ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
+ // }
const mark = state.schema.mark(mark_type);
- return ref_node.marks.includes(mark);
+ return mark.isInSet(ref_node.marks);
});
}
}
@@ -270,56 +263,66 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
setActiveMarkButtons(activeMarks: MarkType[] | undefined) {
if (!activeMarks) return;
- this.boldActive = false;
- this.italicsActive = false;
- this.underlineActive = false;
- this.strikethroughActive = false;
- this.subscriptActive = false;
- this.superscriptActive = false;
+ this._boldActive = false;
+ this._italicsActive = false;
+ this._underlineActive = false;
+ this._strikethroughActive = false;
+ this._subscriptActive = false;
+ this._superscriptActive = false;
activeMarks.forEach(mark => {
switch (mark.name) {
- case "strong": this.boldActive = true; break;
- case "em": this.italicsActive = true; break;
- case "underline": this.underlineActive = true; break;
- case "strikethrough": this.strikethroughActive = true; break;
- case "subscript": this.subscriptActive = true; break;
- case "superscript": this.superscriptActive = true; break;
+ case "strong": this._boldActive = true; break;
+ case "em": this._italicsActive = true; break;
+ case "underline": this._underlineActive = true; break;
+ case "strikethrough": this._strikethroughActive = true; break;
+ case "subscript": this._subscriptActive = true; break;
+ case "superscript": this._superscriptActive = true; break;
}
});
}
- toggleBold = (view: EditorView, forceBool?: boolean) => {
- const mark = view.state.schema.mark(view.state.schema.marks.strong, { strong: forceBool });
- this.setMark(mark, view.state, view.dispatch, false);
- view.focus();
+ toggleBold = () => {
+ if (this.view) {
+ const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong);
+ this.setMark(mark, this.view.state, this.view.dispatch, false);
+ this.view.focus();
+ }
}
- toggleUnderline = (view: EditorView, forceBool?: boolean) => {
- const mark = view.state.schema.mark(view.state.schema.marks.underline, { underline: forceBool });
- this.setMark(mark, view.state, view.dispatch, false);
- view.focus();
+ toggleUnderline = () => {
+ if (this.view) {
+ const mark = this.view.state.schema.mark(this.view.state.schema.marks.underline);
+ this.setMark(mark, this.view.state, this.view.dispatch, false);
+ this.view.focus();
+ }
}
- toggleItalic = (view: EditorView, forceBool?: boolean) => {
- const mark = view.state.schema.mark(view.state.schema.marks.em, { em: forceBool });
- this.setMark(mark, view.state, view.dispatch, false);
- view.focus();
+ toggleItalics = () => {
+ if (this.view) {
+ const mark = this.view.state.schema.mark(this.view.state.schema.marks.em);
+ this.setMark(mark, this.view.state, this.view.dispatch, false);
+ this.view.focus();
+ }
}
- setFontSize = (size: number, view: EditorView) => {
- const fmark = view.state.schema.marks.pFontSize.create({ fontSize: size });
- this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true);
- view.focus();
- this.updateMenu(view, undefined, this.props);
+ setFontSize = (fontSize: string) => {
+ if (this.view) {
+ const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize });
+ this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
+ this.view.focus();
+ this.updateMenu(this.view, undefined, this.props);
+ }
}
- setFontFamily = (family: string, view: EditorView) => {
- const fmark = view.state.schema.marks.pFontFamily.create({ family: family });
- this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true);
- view.focus();
- this.updateMenu(view, undefined, this.props);
+ setFontFamily = (family: string) => {
+ if (this.view) {
+ const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family });
+ this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
+ this.view.focus();
+ this.updateMenu(this.view, undefined, this.props);
+ }
}
setHighlight(color: String, view: EditorView, dispatch: any) {
@@ -330,13 +333,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
setColor(color: String, view: EditorView, dispatch: any) {
- const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color });
- if (view.state.selection.empty) {
- dispatch(view.state.tr.addStoredMark(colorMark));
- return false;
+ if (this.view) {
+ const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color });
+ this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true);
+ view.focus();
+ this.updateMenu(this.view, undefined, this.props);
}
- this.setMark(colorMark, view.state, dispatch, true);
- view.focus();
}
// TODO: remove doesn't work
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 3fd7d61fa..711136469 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -282,7 +282,7 @@ export class RichTextRules {
if (rstate) {
this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
}
- const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500, }, docid);
+ const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: rawdocid.replace(/^:/, ""), _width: 500, _height: 500, }, docid);
DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, "portal to", undefined);
const fstate = this.TextBox.EditorView?.state;
@@ -290,7 +290,7 @@ export class RichTextRules {
this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
}
});
- return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2);
+ return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
}
return state.tr;
}
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 655ee7e44..6103a28d6 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -61,13 +61,13 @@ export const marks: { [index: string]: MarkSpec } = {
/** FONT SIZES */
pFontSize: {
- attrs: { fontSize: { default: 10 } },
+ attrs: { fontSize: { default: "10px" } },
parseDOM: [{
tag: "span", getAttrs(dom: any) {
- return { fontSize: dom.style.fontSize ? Number(dom.style.fontSize.replace("px", "")) : "" };
+ return { fontSize: dom.style.fontSize ? dom.style.fontSize.toString() : "" };
}
}],
- toDOM: (node) => node.attrs.fontSize ? ['span', { style: `font-size: ${node.attrs.fontSize}px;` }] : ['span', 0]
+ toDOM: (node) => node.attrs.fontSize ? ['span', { style: `font-size: ${node.attrs.fontSize};` }] : ['span', 0]
},
/* FONTS */
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index f34b4a8ac..14d6e8be6 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -30,8 +30,7 @@ import { LightboxView } from "../../LightboxView";
import { CollectionFreeFormDocumentView } from "../CollectionFreeFormDocumentView";
import { FieldView, FieldViewProps } from '../FieldView';
import "./PresBox.scss";
-import Color = require("color");
-import { PresEffect, PresStatus, PresMovement } from "./PresEnums";
+import { PresEffect, PresMovement, PresStatus } from "./PresEnums";
export class PinProps {
audioRange?: boolean;
@@ -411,7 +410,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
} else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) {
LightboxView.SetLightboxDoc(undefined);
//awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true); // documents open in new tab instead of on right
+ await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true, NumCast(curDoc.presZoom)); // documents open in new tab instead of on right
}
// After navigating to the document, if it is added as a presPinView then it will
// adjust the pan and scale to that of the pinView when it was added.
@@ -1036,6 +1035,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
Array.from(this._selectedArray.keys()).forEach((doc) => doc.presTransition = timeInMS);
}
+ // Converts seconds to ms and updates presTransition
+ setZoom = (number: String, change?: number) => {
+ let scale = Number(number) / 100;
+ if (change) scale += change;
+ if (scale < 0.01) scale = 0.01;
+ if (scale > 1.5) scale = 1.5;
+ Array.from(this._selectedArray.keys()).forEach((doc) => doc.presZoom = scale);
+ }
+
// Converts seconds to ms and updates presDuration
setDurationTime = (number: String, change?: number) => {
let timeInMS = Number(number) * 1000;
@@ -1153,11 +1161,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
@computed get transitionDropdown() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
- const type = targetDoc.type;
const isPresCollection: boolean = (targetDoc === this.layoutDoc.presCollection);
const isPinWithView: boolean = BoolCast(activeItem.presPinView);
if (activeItem && targetDoc) {
+ const type = targetDoc.type;
const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5;
+ const zoom = activeItem.presZoom ? NumCast(activeItem.presZoom) * 100 : 75;
let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2;
if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
const effect = targetDoc.presEffect ? targetDoc.presEffect : 'None';
@@ -1182,6 +1191,31 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
</div>
</div>
}
+ <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}>
+ <div className="presBox-subheading">Zoom (% screen filled)</div>
+ <div className="ribbon-property">
+ <input className="presBox-input"
+ type="number" value={zoom}
+ onChange={action((e) => this.setZoom(e.target.value))} />%
+ </div>
+ <div className="ribbon-propertyUpDown">
+ <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
+ <FontAwesomeIcon icon={"caret-up"} />
+ </div>
+ <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}>
+ <FontAwesomeIcon icon={"caret-down"} />
+ </div>
+ </div>
+ </div>
+ <input type="range" step="1" min="0" max="150" value={zoom}
+ className={`toolbar-slider ${activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}
+ id="toolbar-slider"
+ onPointerDown={() => this._batch = UndoManager.StartBatch("presZoom")}
+ onPointerUp={() => this._batch?.end()}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ e.stopPropagation();
+ this.setZoom(e.target.value);
+ }} />
<div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}>
<div className="presBox-subheading">Movement Speed</div>
<div className="ribbon-property">
diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss
index e98f071c3..1de60ffed 100644
--- a/src/client/views/pdf/Annotation.scss
+++ b/src/client/views/pdf/Annotation.scss
@@ -4,4 +4,7 @@
position: absolute;
background-color: rgba(146, 245, 95, 0.467);
transition: opacity 0.5s;
+ &:hover {
+ cursor: pointer;
+ }
} \ No newline at end of file
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 07b734405..b1d1d8293 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -16,17 +16,19 @@ interface IAnnotationProps extends FieldViewProps {
dataDoc: Doc;
fieldKey: string;
showInfo: (anno: Opt<Doc>) => void;
+ pointerEvents?: string;
}
@observer
export
class Annotation extends React.Component<IAnnotationProps> {
render() {
- return DocListCast(this.props.anno.textInlineAnnotations).map(a => <RegionAnnotation {...this.props} document={a} key={a[Id]} />);
+ return DocListCast(this.props.anno.textInlineAnnotations).map(a => <RegionAnnotation pointerEvents={this.props.pointerEvents} {...this.props} document={a} key={a[Id]} />);
}
}
interface IRegionAnnotationProps extends IAnnotationProps {
document: Doc;
+ pointerEvents?: string;
}
@observer
class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
@@ -96,6 +98,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
width: NumCast(this.props.document._width),
height: NumCast(this.props.document._height),
opacity: this._brushed ? 0.5 : undefined,
+ pointerEvents: this.props.pointerEvents as any,
backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.backgroundColor),
}} >
</div>);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index d953c6b6c..3f7f38bdf 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -16,10 +16,13 @@ import { CompiledScript, CompileScript } from "../../util/Scripting";
import { SelectionManager } from "../../util/SelectionManager";
import { SharingManager } from "../../util/SharingManager";
import { SnappingManager } from "../../util/SnappingManager";
+import { MarqueeOptionsMenu } from "../collections/collectionFreeForm";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { MarqueeAnnotator } from "../MarqueeAnnotator";
+import { DocumentViewProps } from "../nodes/DocumentView";
import { FieldViewProps } from "../nodes/FieldView";
import { LinkDocPreview } from "../nodes/LinkDocPreview";
+import { StyleProp } from "../StyleProvider";
import { AnchorMenu } from "./AnchorMenu";
import { Annotation } from "./Annotation";
import "./PDFViewer.scss";
@@ -125,12 +128,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
});
- this._disposers.searchMatch = reaction(() => Doc.IsSearchMatch(this.props.rootDoc),
- m => {
- if (m) (this._lastSearch = true) && this.search(Doc.SearchQuery(), m.searchMatch > 0);
- else !(this._lastSearch = false) && setTimeout(() => !this._lastSearch && this.search("", false, true), 200);
- }, { fireImmediately: true });
-
this._disposers.selected = reaction(() => this.props.isSelected(),
selected => {
// if (!selected) {
@@ -185,7 +182,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
let focusSpeed: Opt<number>;
if (doc !== this.props.rootDoc && mainCont) {
const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1);
- const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, .1 * windowHeight);
+ const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, .1 * windowHeight);
if (scrollTo !== undefined) {
focusSpeed = 500;
@@ -337,10 +334,10 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
@action
- search = (searchString: string, fwd: boolean, clear: boolean = false) => {
+ search = (searchString: string, bwd?: boolean, clear: boolean = false) => {
const findOpts = {
caseSensitive: false,
- findPrevious: !fwd,
+ findPrevious: bwd,
highlightAll: true,
phraseSearch: true,
query: searchString
@@ -348,7 +345,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (clear) {
this._pdfViewer?.findController.executeCommand('reset', { query: "" });
} else if (!searchString) {
- fwd ? this.nextAnnotation() : this.prevAnnotation();
+ bwd ? this.prevAnnotation() : this.nextAnnotation();
} else if (this._pdfViewer?.pageViewsReady) {
this._pdfViewer.findController.executeCommand('findagain', findOpts);
}
@@ -357,6 +354,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
this._mainCont.current.addEventListener("pagesloaded", executeFind);
this._mainCont.current.addEventListener("pagerendered", executeFind);
}
+ return true;
}
@action
@@ -486,11 +484,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
}
+ pointerEvents = () => this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none";
@computed get annotationLayer() {
+ const pe = this.pointerEvents();
return <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}>
{this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno =>
- <Annotation {...this.props} fieldKey={this.props.fieldKey + "-annotations"} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)
- }
+ <Annotation {...this.props} fieldKey={this.props.fieldKey + "-annotations"} pointerEvents={pe} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)}
</div>;
}
@@ -507,8 +506,16 @@ export class PDFViewer extends React.Component<IViewerProps> {
overlayTransform = () => this.scrollXf().scale(1 / this._zoomed);
panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+ basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter("textInlineAnnotations")];
transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+ childStyleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => {
+ if (doc instanceof Doc && property === StyleProp.PointerEvents) {
+ if (doc.textInlineAnnotations) return "none";
+ return "all";
+ }
+ return this.props.styleProvider?.(doc, props, property);
+ }
@computed get overlayLayer() {
const renderAnnotations = (docFilters?: () => string[]) =>
<CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
@@ -521,12 +528,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
select={emptyFunction}
ContentScaling={this.contentZoom}
bringToFront={emptyFunction}
- docFilters={docFilters || this.props.docFilters}
+ docFilters={docFilters || this.basicFilter}
+ styleProvider={this.childStyleProvider}
dontRenderDocuments={docFilters ? false : true}
CollectionView={undefined}
ScreenToLocalTransform={this.overlayTransform}
- renderDepth={this.props.renderDepth + 1}
- childPointerEvents={true} />;
+ renderDepth={this.props.renderDepth + 1} />;
return <div>
<div className={`pdfViewerDash-overlay${CurrentUserUtils.SelectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`}
style={{
@@ -548,7 +555,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
</div>;
}
@computed get pdfViewerDiv() {
- return <div className={"pdfViewerDash-text" + (this._textSelecting && (this.props.isSelected() || this.props.isContentActive()) ? "-selected" : "")} ref={this._viewer} />;
+ return <div className={"pdfViewerDash-text" + (this.props.pointerEvents !== "none" && this._textSelecting && (this.props.isSelected() || this.props.isContentActive()) ? "-selected" : "")} ref={this._viewer} />;
}
@computed get contentScaling() { return this.props.ContentScaling?.() || 1; }
@computed get standinViews() {
@@ -561,7 +568,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
render() {
TraceMobx();
return <div className="pdfViewer-content">
- <div className={`pdfViewerDash${this.props.isContentActive() ? "-interactive" : ""}`} ref={this._mainCont}
+ <div className={`pdfViewerDash${this.props.isContentActive() && this.props.pointerEvents !== "none" ? "-interactive" : ""}`} ref={this._mainCont}
onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}
style={{
overflowX: this._zoomed !== 1 ? "scroll" : undefined,
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 9c353e9d0..3612bd7c4 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -104,9 +104,9 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
* This method is called when the user clicks on a search result. The _selectedResult is
* updated accordingly and the doc is highlighted with the selectElement method.
*/
- onResultClick = action((doc: Doc) => {
- this.selectElement(doc);
+ onResultClick = action(async (doc: Doc) => {
this._selectedResult = doc;
+ this.selectElement(doc, () => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, false));
});
makeLink = action((linkTo: Doc) => {
@@ -269,8 +269,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
* This method selects a doc by either jumping to it (centering/zooming in on it)
* or opening it in a new tab.
*/
- selectElement = async (doc: Doc) => {
- await DocumentManager.Instance.jumpToDocument(doc, true);
+ selectElement = async (doc: Doc, finishFunc: () => void) => {
+ await DocumentManager.Instance.jumpToDocument(doc, true, undefined, undefined, undefined, undefined, undefined, finishFunc);
}
/**
@@ -307,7 +307,12 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
validResults++;
return (
<Tooltip key={result[0][Id]} placement={"right"} title={<><div className="dash-tooltip">{title}</div></>}>
- <div onClick={isLinkSearch ? () => this.makeLink(result[0]) : () => this.onResultClick(result[0])} className={className}>
+ <div onClick={isLinkSearch ?
+ () => this.makeLink(result[0]) :
+ e => {
+ this.onResultClick(result[0]);
+ e.stopPropagation();
+ }} className={className}>
<div className="searchBox-result-title">
{title}
</div>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 49dfb14a7..328385fda 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1,15 +1,17 @@
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { saveAs } from "file-saver";
import { action, computed, observable, ObservableMap, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import { alias, map, serializable } from "serializr";
import { DocServer } from "../client/DocServer";
import { DocumentType } from "../client/documents/DocumentTypes";
+import { CurrentUserUtils } from "../client/util/CurrentUserUtils";
import { LinkManager } from "../client/util/LinkManager";
import { Scripting, scriptingGlobal } from "../client/util/Scripting";
import { SelectionManager } from "../client/util/SelectionManager";
import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
import { UndoManager } from "../client/util/UndoManager";
-import { intersectRect, Utils } from "../Utils";
+import { DashColor, intersectRect, Utils } from "../Utils";
import { DateField } from "./DateField";
import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols";
import { List } from "./List";
@@ -23,9 +25,6 @@ import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from "./URLField";
import { deleteProperty, GetEffectiveAcl, getField, getter, inheritParentAcls, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util";
import JSZip = require("jszip");
-import { CurrentUserUtils } from "../client/util/CurrentUserUtils";
-import { IconProp } from "@fortawesome/fontawesome-svg-core";
-import Color = require("color");
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -80,6 +79,7 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) {
export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { return Cast(field, Doc); }
+export function NumListCast(field: FieldResult) { return Cast(field, listSpec("number"), []); }
export function StrListCast(field: FieldResult) { return Cast(field, listSpec("string"), []); }
export function DocListCast(field: FieldResult) { return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; }
export function DocListCastOrNull(field: FieldResult) { return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; }
@@ -1090,7 +1090,7 @@ export namespace Doc {
export function matchFieldValue(doc: Doc, key: string, value: any): boolean {
if (Utils.HasTransparencyFilter(value)) {
- const isTransparent = (color: string) => color !== "" && (Color(color).alpha() !== 1);
+ const isTransparent = (color: string) => color !== "" && (DashColor(color).alpha() !== 1);
return isTransparent(StrCast(doc[key]));
}
if (typeof value === "string") {
@@ -1137,14 +1137,14 @@ export namespace Doc {
// filters document in a container collection:
// all documents with the specified value for the specified key are included/excluded
// based on the modifiers :"check", "x", undefined
- export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: "remove" | "match" | "check" | "x" | "exists", toggle?: boolean, fieldSuffix?: string, append: boolean = true) {
+ export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: "remove" | "match" | "check" | "x" | "exists" | "unset", toggle?: boolean, fieldSuffix?: string, append: boolean = true) {
if (!container) return;
const filterField = "_" + (fieldSuffix ? fieldSuffix + "-" : "") + "docFilters";
const docFilters = Cast(container[filterField], listSpec("string"), []);
runInAction(() => {
for (let i = 0; i < docFilters.length; i++) {
const fields = docFilters[i].split(":"); // split key:value:modifier
- if (fields[0] === key && (fields[1] === value || modifiers === "match" || modifiers === "remove")) {
+ if (fields[0] === key && (fields[1] === value || modifiers === "match")) {
if (fields[2] === modifiers && modifiers && fields[1] === value) {
if (toggle) modifiers = "remove";
else return;
@@ -1316,6 +1316,7 @@ export namespace Doc {
if (typeof resolved === "object" && !(resolved instanceof Array)) {
output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc);
} else {
+ // give the proper types to the data extracted from the JSON
const result = toField(resolved, excludeEmptyObjects);
if (appendToExisting) {
(output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result;
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index 62734d3d2..f01b502c9 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -79,7 +79,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
return field;
}));
}
- return this.promise as any;
+ return DocServer.GetCachedRefField(this.fieldId) ?? (this.promise as any);
}
promisedValue(): string { return !this.cache && !this.failed && !this.promise ? this.fieldId : ""; }
setPromise(promise: any) {
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index 0b5f14d74..a19be5df9 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -2,7 +2,7 @@ import { AssertionError } from "assert";
import { docs_v1 } from "googleapis";
import { Fragment, Mark, Node } from "prosemirror-model";
import { sinkListItem } from "prosemirror-schema-list";
-import { Utils } from "../Utils";
+import { Utils, DashColor } from "../Utils";
import { Docs, DocUtils } from "../client/documents/Documents";
import { schema } from "../client/views/nodes/formattedText/schema_rts";
import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
@@ -482,7 +482,7 @@ export namespace RichTextUtils {
}
const fromHex = (color: string): docs_v1.Schema$OptionalColor => {
- const c = Color(color);
+ const c = DashColor(color);
return fromRgb.convert(c.red(), c.green(), c.blue());
};
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 439c4d333..c708affe3 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -98,13 +98,12 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
} else {
DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
}
- !receiver[Initializing] && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent({
- redo: () => receiver[prop] = value,
- undo: () => {
- // console.log("Undo: " + prop + " = " + curValue); // bcz: uncomment to log undo
- receiver[prop] = curValue;
- }
- });
+ !receiver[Initializing] && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) &&
+ UndoManager.AddEvent({
+ redo: () => receiver[prop] = value,
+ undo: () => receiver[prop] = curValue,
+ prop: prop?.toString()
+ });
return true;
}
return false;
@@ -191,7 +190,8 @@ let HierarchyMapping: Map<symbol, number> | undefined;
function getEffectiveAcl(target: any, user?: string): symbol {
const targetAcls = target[AclSym];
const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group
- if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; // target may be a Doc of Proxy, so check __fields.author and .author
+ const targetAuthor = (target.__fields?.author || target.author); // target may be a Doc of Proxy, so check __fields.author and .author
+ if (userChecked === targetAuthor || !targetAuthor) return AclAdmin;
if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin;
if (targetAcls && Object.keys(targetAcls).length) {
@@ -405,7 +405,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
ind !== -1 && receiver[prop].splice(ind, 1);
});
lastValue = ObjectField.MakeCopy(receiver[prop]);
- })
+ }),
+ prop: ""
} :
diff?.op === "$remFromSet" ?
{
@@ -423,7 +424,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item);
});
lastValue = ObjectField.MakeCopy(receiver[prop]);
- }
+ },
+ prop: ""
}
: {
redo: () => {
@@ -434,7 +436,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
// console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo
receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>);
lastValue = ObjectField.MakeCopy(receiver[prop]);
- }
+ },
+ prop: ""
});
}
target[Update](op);
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 404e828ea..652804126 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -17,7 +17,7 @@ import { Docs, DocumentOptions, DocUtils } from '../client/documents/Documents';
import { DocumentType } from "../client/documents/DocumentTypes";
import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
import { Scripting } from '../client/util/Scripting';
-import { SettingsManager } from '../client/util/SettingsManager';
+import { SettingsManager, ColorScheme } from '../client/util/SettingsManager';
import { Transform } from '../client/util/Transform';
import { UndoManager } from "../client/util/UndoManager";
import { TabDocView } from '../client/views/collections/TabDocView';
@@ -403,7 +403,7 @@ export class MobileInterface extends React.Component {
const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row");
- const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`);
+ const toggleTheme = ScriptField.MakeScript(`self.colorScheme = self.colorScheme ? undefined: ${ColorScheme.Dark}}`);
const toggleComic = ScriptField.MakeScript(`toggleComicMode()`);
const cloneDashboard = ScriptField.MakeScript(`cloneDashboard()`);
dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, cloneDashboard!]);
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 7b83d09ef..54b71e8ce 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -3,6 +3,7 @@ import { ExifImage } from 'exif';
import { File } from 'formidable';
import { createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs';
import * as path from 'path';
+import * as exifr from 'exifr';
import { basename } from "path";
import * as sharp from 'sharp';
import { Stream } from 'stream';
@@ -342,7 +343,8 @@ export namespace DashUploadUtils {
resolve({ data, error: reason });
});
});
- data && bufferConverterRec(data);
+ return { data: await exifr.parse(image) as any, error };
+ //data && bufferConverterRec(data);
return { data, error };
};
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 0f4a067fc..de93b64c3 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -13,6 +13,7 @@ import * as request from 'request';
import * as webpack from 'webpack';
import * as wdm from 'webpack-dev-middleware';
import * as whm from 'webpack-hot-middleware';
+import * as zlib from 'zlib';
import { publicDirectory } from '.';
import { logPort } from './ActionUtilities';
import { SSL } from './apis/google/CredentialsLoader';
@@ -36,37 +37,30 @@ export let resolvedPorts: { server: number, socket: number } = { server: 1050, s
export let resolvedServerUrl: string;
export default async function InitializeServer(routeSetter: RouteSetter) {
+ const isRelease = determineEnvironment();
const app = buildWithMiddleware(express());
- app.use(express.static(publicDirectory, {
- setHeaders: res => res.setHeader("Access-Control-Allow-Origin", "*")
- }));
- app.use("/images", express.static(publicDirectory));
+ // route table managed by express. routes are tested sequentially against each of these map rules. when a match is found, the handler is called to process the request
+ app.get(new RegExp(/^\/+$/), (req, res) => res.redirect(req.user ? "/home" : "/login")); // target urls that consist of one or more '/'s with nothing in between
+ app.use(express.static(publicDirectory, { setHeaders: res => res.setHeader("Access-Control-Allow-Origin", "*") })); //all urls that start with dash's public directory: /files/ (e.g., /files/images, /files/audio, etc)
app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) }));
-
app.use(wdm(compiler, { publicPath: config.output.publicPath }));
app.use(whm(compiler));
-
- registerAuthenticationRoutes(app);
- registerCorsProxy(app);
-
- const isRelease = determineEnvironment();
-
+ registerAuthenticationRoutes(app); // this adds routes to authenticate a user (login, etc)
+ registerCorsProxy(app); // this adds a /corsProxy/ route to allow clients to get to urls that would otherwise be blocked by cors policies
isRelease && !SSL.Loaded && SSL.exit();
-
- routeSetter(new RouteManager(app, isRelease));
- registerRelativePath(app);
+ routeSetter(new RouteManager(app, isRelease)); // this sets up all the regular supervised routes (things like /home, download/upload api's, pdf, search, session, etc)
+ registerEmbeddedBrowseRelativePathHandler(app); // this allows renered web pages which internally have relative paths to find their content
let server: HttpServer | HttpsServer;
- const { serverPort, serverName } = process.env;
- isRelease && serverPort && (resolvedPorts.server = Number(serverPort));
+ isRelease && process.env.serverPort && (resolvedPorts.server = Number(process.env.serverPort));
await new Promise<void>(resolve => server = isRelease ?
createServer(SSL.Credentials, app).listen(resolvedPorts.server, resolve) :
app.listen(resolvedPorts.server, resolve)
);
logPort("server", resolvedPorts.server);
- resolvedServerUrl = `${isRelease && serverName ? `https://${serverName}.com` : "http://localhost"}:${resolvedPorts.server}`;
+ resolvedServerUrl = `${isRelease && process.env.serverName ? `https://${process.env.serverName}.com` : "http://localhost"}:${resolvedPorts.server}`;
// initialize the web socket (bidirectional communication: if a user changes
// a field on one client, that change must be broadcast to all other clients)
@@ -139,54 +133,81 @@ function registerAuthenticationRoutes(server: express.Express) {
}
function registerCorsProxy(server: express.Express) {
- const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
server.use("/corsProxy", async (req, res) => {
-
const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : "";
- const requrlraw = decodeURIComponent(req.url.substring(1));
+ let requrlraw = decodeURIComponent(req.url.substring(1));
+ const qsplit = requrlraw.split("?q=");
+ const newqsplit = requrlraw.split("&q=");
+ if (qsplit.length > 1 && newqsplit.length > 1) {
+ const lastq = newqsplit[newqsplit.length - 1];
+ requrlraw = qsplit[0] + "?q=" + lastq.split("&")[0] + "&" + qsplit[1].split("&")[1];
+ }
const requrl = requrlraw.startsWith("/") ? referer + requrlraw : requrlraw;
- // cors weirdness here...
- // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative,
+ // cors weirdness here...
+ // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative,
// then we redirect again to the cors referer and just add the relative path.
if (!requrl.startsWith("http") && req.originalUrl.startsWith("/corsProxy") && referer?.includes("corsProxy")) {
res.redirect(referer + (referer.endsWith("/") ? "" : "/") + requrl);
} else {
- try {
- await new Promise<void>((resolve, reject) => {
- request(requrl).on("response", resolve).on("error", reject);
- });
- } catch {
- console.log(`Malformed CORS url: ${requrl}`);
- return res.send();
- }
- req.pipe(request(requrl)).on("response", res => {
- const headers = Object.keys(res.headers);
- headers.forEach(headerName => {
- const header = res.headers[headerName];
- if (Array.isArray(header)) {
- res.headers[headerName] = header.filter(h => !headerCharRegex.test(h));
- } else if (header) {
- if (headerCharRegex.test(header as any)) {
- delete res.headers[headerName];
- }
- }
- });
- }).on("error", () => console.log(`Malformed CORS url: ${requrl}`)).pipe(res);
+ proxyServe(req, requrl, res);
}
});
}
-function registerRelativePath(server: express.Express) {
+function proxyServe(req: any, requrl: string, response: any) {
+ const htmlBodyMemoryStream = new (require('memorystream'))();
+ req.headers.cookie = "";
+ req.pipe(request(requrl))
+ .on("error", (e: any) => console.log(`Malformed CORS url: ${requrl}`, e))
+ .on("end", () => {
+ var rewrittenHtmlBody: any = undefined;
+ req.pipe(request(requrl))
+ .on("response", (res: any) => {
+ const headers = Object.keys(res.headers);
+ const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
+ headers.forEach(headerName => {
+ const header = res.headers[headerName];
+ if (Array.isArray(header)) {
+ res.headers[headerName] = header.filter(h => !headerCharRegex.test(h));
+ } else if (headerCharRegex.test(header || "")) {
+ delete res.headers[headerName];
+ }
+ if (headerName === "content-encoding" && header.includes("gzip")) {
+ try {
+ const replacer = (match: any, href: string, offset: any, string: any) => {
+ return `href="${resolvedServerUrl + "/corsProxy/http" + href}"`;
+ };
+ const zipToStringDecoder = new (require('string_decoder').StringDecoder)('utf8');
+ const htmlText = zipToStringDecoder.write(zlib.gunzipSync(htmlBodyMemoryStream.read()).toString('utf8')
+ .replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>')
+ .replace(/href="http([^"]*)"/g, replacer)
+ .replace(/target="_blank"/g, ""));
+ rewrittenHtmlBody = zlib.gzipSync(htmlText);
+ } catch (e) { console.log(e); }
+ }
+ });
+ })
+ .on('data', (e: any) => {
+ rewrittenHtmlBody && response.send(rewrittenHtmlBody);
+ rewrittenHtmlBody = undefined;
+ })
+ .pipe(response);
+ })
+ .pipe(htmlBodyMemoryStream);
+}
+
+function registerEmbeddedBrowseRelativePathHandler(server: express.Express) {
server.use("*", (req, res) => {
const relativeUrl = req.originalUrl;
- if (!res.headersSent && req.headers.referer?.includes("corsProxy")) { // a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here.
+ if (!req.user) res.redirect("/home"); // When no user is logged in, we interpret a relative URL as being a reference to something they don't have access to and redirect to /home
+ else if (!res.headersSent && req.headers.referer?.includes("corsProxy")) { // a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here.
const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:<port>/corsProxy/https://en.wikipedia.org/wiki/Engelbart)
const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:<port>/corsProxy/ )
const actualReferUrl = proxiedRefererUrl.replace(dashServerUrl, ""); // the url of the referer without the proxy (e.g., : http:s//en.wikipedia.org/wiki/Engelbart)
const absoluteTargetBaseUrl = actualReferUrl.match(/http[s]?:\/\/[^\/]*/)![0]; // the base of the original url (e.g., https://en.wikipedia.org)
const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e..g, http://localhost:<port>/corsProxy/https://en.wikipedia.org/<somethingelse>)
res.redirect(redirectedProxiedUrl);
- } else if (relativeUrl.startsWith("/search")) { // detect search query and use default search engine
+ } else if (relativeUrl.startsWith("/search") && !req.headers.referer?.includes("corsProxy")) { // detect search query and use default search engine
res.redirect(req.headers.referer + "corsProxy/" + encodeURIComponent("http://www.google.com" + relativeUrl));
} else {
res.end();