aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts1
-rw-r--r--src/client/util/DragManager.ts2
-rw-r--r--src/client/views/DocumentButtonBar.tsx16
-rw-r--r--src/client/views/DocumentDecorations.scss7
-rw-r--r--src/client/views/DocumentDecorations.tsx20
-rw-r--r--src/client/views/StyleProvider.tsx2
-rw-r--r--src/client/views/TagsView.tsx69
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx1
-rw-r--r--src/client/views/collections/CollectionCarouselView.scss46
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx359
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx3
-rw-r--r--src/client/views/collections/TabDocView.tsx175
-rw-r--r--src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx4
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx4
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts2
-rw-r--r--src/client/views/search/FaceRecognitionHandler.tsx6
-rw-r--r--src/fields/Doc.ts8
18 files changed, 375 insertions, 352 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 49ecf9333..da3419de2 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -274,6 +274,7 @@ export class DocumentOptions {
_layout_showTitle?: string; // field name to display in header (:hover is an optional suffix)
_layout_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts');
_layout_showCaption?: string; // which field to display in the caption area. leave empty to have no caption
+ _layout_showTags?: BOOLt = new BoolInfo('whether to show the list of document tags at the bottom of a DocView');
_chromeHidden?: BOOLt = new BoolInfo('whether the editing chrome for a document is hidden');
hideClickBehaviors?: BOOLt = new BoolInfo('whether to hide click behaviors in context menu');
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 7db13689d..d55d193cc 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -569,7 +569,7 @@ export namespace DragManager {
AbortDrag();
await finishDrag?.(new DragCompleteEvent(true, docDragData));
DragManager.StartWindowDrag?.(e, docDragData.droppedDocuments, aborted => {
- if (!aborted && (docDragData?.dropAction === 'move' || docDragData?.dropAction === 'same')) {
+ if (!aborted && (docDragData?.dropAction === dropActionType.move || docDragData?.dropAction === dropActionType.same)) {
docDragData.removeDocument?.(docDragData?.draggedDocuments[0]);
}
});
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 437ef045f..f14fd033b 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -9,7 +9,7 @@ import * as React from 'react';
import { FaEdit } from 'react-icons/fa';
import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../ClientUtils';
import { emptyFunction } from '../../Utils';
-import { Doc } from '../../fields/Doc';
+import { Doc, DocListCast } from '../../fields/Doc';
import { Cast, DocCast } from '../../fields/Types';
import { DocUtils, IsFollowLinkScript } from '../documents/DocUtils';
import { CalendarManager } from '../util/CalendarManager';
@@ -267,7 +267,19 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
get keywordButton() {
return !DocumentView.Selected().length ? null : (
<Tooltip title={<div className="dash-keyword-button">Open keyword menu</div>}>
- <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={() => DocumentView.Selected().map(dv => (dv.dataDoc.showTags = !dv.dataDoc.showTags))}>
+ <div
+ className="documentButtonBar-icon"
+ style={{ color: 'white' }}
+ onClick={undoable(e => {
+ const showing = DocumentView.Selected().some(dv => dv.layoutDoc._layout_showTags);
+ DocumentView.Selected().forEach(dv => {
+ dv.layoutDoc._layout_showTags = !showing;
+ if (e.shiftKey)
+ DocListCast(dv.Document[Doc.LayoutFieldKey(dv.Document) + '_annotations']).forEach(doc => {
+ if (doc.face) doc.hidden = showing;
+ });
+ });
+ }, 'show Doc tags')}>
<FontAwesomeIcon className="documentdecorations-icon" icon="tag" />
</div>
</Tooltip>
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 67e1054c3..346df10d5 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -38,6 +38,13 @@ $resizeHandler: 8px;
background: green;
border-radius: 50%;
}
+ .documentDecorations-tagsView {
+ position: absolute;
+ height: 100%;
+ pointer-events: all;
+ border-radius: 50%;
+ color: black;
+ }
}
.documentDecorations-container {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index da35459bb..5e7908725 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -35,6 +35,7 @@ import { DocumentView } from './nodes/DocumentView';
import { ImageBox } from './nodes/ImageBox';
import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { TagsView } from './TagsView';
interface DocumentDecorationsProps {
PanelWidth: number;
@@ -172,7 +173,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
!this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('$') ? (selected && Field.toKeyValueString(selected, this._titleControlString.substring(1))) || '-unset-' : this._titleControlString);
this._editingTitle = true;
this._keyinput.current && setTimeout(this._keyinput.current.focus);
- })
+ }),
+ false // can't preventDefault since that will mess up goldenlayout if you drag over the tab bar. so just stop propagation below.
);
e.stopPropagation();
}
@@ -194,7 +196,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
if (containers.size > 1) return false;
const { left, top } = dragDocView.getBounds || { left: 0, top: 0 };
const dragData = new DragManager.DocumentDragData(DocumentView.SelectedDocs(), dragDocView._props.dropAction);
- dragData.offset = dragDocView.screenToContentsTransform().transformDirection(e.x - left, e.y - top);
+ dragData.offset = dragDocView.screenToViewTransform().transformDirection(e.x - left, e.y - top);
dragData.moveDocument = dragDocView._props.moveDocument;
dragData.removeDocument = dragDocView._props.removeDocument;
dragData.isDocDecorationMove = true;
@@ -231,7 +233,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
if (iconViewDoc.activeFrame) {
iconViewDoc.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation.
} else {
- iconView._props.removeDocument?.(iconView.Document);
+ if (iconView.Document.annotationOn && iconView.Document.face) iconView.Document.hidden = true;
+ else iconView._props.removeDocument?.(iconView.Document);
}
});
views.forEach(DocumentView.DeselectView);
@@ -643,7 +646,6 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
render() {
const { b, r, x, y } = this.Bounds;
const seldocview = DocumentView.Selected().lastElement();
- const doc = DocumentView.SelectedDocs().lastElement();
if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
setTimeout(
action(() => {
@@ -835,12 +837,20 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
<div
className="link-button-container"
style={{
- top: `${doc[DocData].showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
+ top: DocumentView.Selected().length > 1 ? 0 : `${seldocview.Document._layout_showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
}}>
<DocumentButtonBar views={() => DocumentView.Selected()} />
</div>
)}
+ <div
+ className="documentDecorations-tagsView"
+ style={{
+ top: `${seldocview.Document._layout_showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
+ transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
+ }}>
+ {DocumentView.Selected().length > 1 ? <TagsView Views={DocumentView.Selected()} /> : null}
+ </div>
</div>
{useRotation && (
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 513953d17..262f888fb 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -365,7 +365,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
</Tooltip>
);
};
- const tags = () => props?.DocumentView?.() ? <TagsView View={props.DocumentView()}/> : null;
+ const tags = () => props?.DocumentView?.() ? <TagsView Views={[props.DocumentView()]}/> : null;
return (
<>
{paint()}
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index 8047363d9..be2c28185 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -33,7 +33,7 @@ import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
*/
interface TagItemProps {
- doc: Doc;
+ docs: Doc[];
tag: string;
tagDoc: Opt<Doc>;
showRemoveUI: boolean;
@@ -172,12 +172,12 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
docData.data = new List<Doc>(newEmbeddings);
docData.title = this._props.tag;
docData.tags = new List<string>([this._props.tag]);
- docData.showTags = true;
docData.freeform_fitContentsToBox = true;
doc._freeform_panX = doc._freeform_panY = 0;
doc._width = 900;
doc._height = 900;
doc.layout_fitWidth = true;
+ doc._layout_showTags = true;
return doc;
})(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true));
newEmbeddings.forEach(embed => Doc.SetContainer(embed, newCollection));
@@ -207,8 +207,12 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
e.preventDefault();
};
+ @computed get doc() {
+ return this._props.docs.lastElement();
+ }
+
render() {
- this._props.tagDoc && setTimeout(() => TagItem.addTagToDoc(this._props.doc, this._props.tag)); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection
+ this._props.tagDoc && setTimeout(() => this._props.docs.forEach(doc => TagItem.addTagToDoc(doc, this._props.tag))); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection
const tag = this._props.tag.replace(/^#/, '');
const metadata = tag.startsWith('@') ? tag.replace(/^@/, '') : '';
return (
@@ -216,16 +220,16 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
{metadata ? (
<span>
<b style={{ fontSize: 'smaller' }}>{tag}&nbsp;</b>
- {typeof this._props.doc[metadata] === 'boolean' ? (
+ {typeof this.doc[metadata] === 'boolean' ? (
<input
type="checkbox"
onClick={e => e.stopPropagation()}
onPointerDown={e => e.stopPropagation()}
- onChange={undoable(e => (this._props.doc[metadata] = !this._props.doc[metadata]), 'metadata toggle')}
- checked={this._props.doc[metadata] as boolean}
+ onChange={undoable(e => (this.doc[metadata] = !this.doc[metadata]), 'metadata toggle')}
+ checked={this.doc[metadata] as boolean}
/>
) : (
- Field.toString(this._props.doc[metadata])
+ Field.toString(this.doc[metadata])
)}
</span>
) : (
@@ -234,7 +238,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
{this.props.showRemoveUI && this._props.tagDoc && (
<IconButton
tooltip="Remove tag"
- onPointerDown={undoable(() => TagItem.removeTagFromDoc(this._props.doc, this._props.tag, this._props.tagDoc), `remove tag ${this._props.tag}`)}
+ onPointerDown={undoable(() => this._props.docs.forEach(doc => TagItem.removeTagFromDoc(doc, this._props.tag, this._props.tagDoc)), `remove tag ${this._props.tag}`)}
icon={<FontAwesomeIcon icon="times" size="sm" />}
style={{ width: '8px', height: '8px', marginLeft: '10px' }}
/>
@@ -245,7 +249,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
}
interface TagViewProps {
- View: DocumentView;
+ Views: DocumentView[];
}
/**
@@ -261,12 +265,12 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
@observable _panelHeightDirty = 0;
@observable _currentInput = '';
- @observable _isEditing = !StrListCast(this._props.View.dataDoc.tags).length;
+ @observable _isEditing = !StrListCast(this.View.dataDoc.tags).length;
_heightDisposer: IReactionDisposer | undefined;
componentDidMount() {
this._heightDisposer = reaction(
- () => this._props.View.screenToContentsTransform(),
+ () => this.View.screenToContentsTransform(),
xf => {
this._panelHeightDirty = this._panelHeightDirty + 1;
}
@@ -276,11 +280,15 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
this._heightDisposer?.();
}
+ @computed get View() {
+ return this._props.Views.lastElement();
+ }
+
@computed get currentScale() {
- return Math.max(1, 1 / this._props.View.screenToLocalScale());
+ return this._props.Views.length > 1 ? 1 : Math.max(1, 1 / this.View.screenToLocalScale());
}
@computed get isEditing() {
- return this._isEditing && DocumentView.SelectedDocs().includes(this._props.View.Document);
+ return this._isEditing && (this._props.Views.length > 1 || DocumentView.SelectedDocs().includes(this.View.Document));
}
/**
@@ -290,7 +298,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
@action
setToEditing = (editing = true) => {
this._isEditing = editing;
- editing && this._props.View.select(false);
+ editing && this._props.Views.length === 1 && this.View.select(false);
};
/**
@@ -303,8 +311,10 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
action((tag: string) => {
const submittedLabel = tag.trim().replace(/^#/, '').split(':');
if (submittedLabel[0]) {
- TagItem.addTagToDoc(this._props.View.Document, '#' + submittedLabel[0]);
- if (submittedLabel.length > 1) Doc.SetField(this._props.View.Document, submittedLabel[0].replace(/^@/, ''), ':' + submittedLabel[1]);
+ this._props.Views.forEach(view => {
+ TagItem.addTagToDoc(view.Document, '#' + submittedLabel[0]);
+ if (submittedLabel.length > 1) Doc.SetField(view.Document, submittedLabel[0].replace(/^@/, ''), ':' + submittedLabel[1]);
+ });
}
this._currentInput = ''; // Clear the input box
}),
@@ -312,24 +322,24 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
);
/**
- * When 'showTags' is set on a Doc, this displays a wrapping panel of tagItemViews corresponding to all the tags set on the Doc).
+ * When 'layout_showTags' is set on a Doc, this displays a wrapping panel of tagItemViews corresponding to all the tags set on the Doc).
* When the dropdown is clicked, this will toggle an extended UI that allows additional tags to be added/removed.
*/
render() {
- const tagsList = new Set<string>(StrListCast(this._props.View.dataDoc.tags));
- const chatTagsList = new Set<string>(StrListCast(this._props.View.dataDoc.tags_chat));
+ const tagsList = new Set<string>(StrListCast(this.View.dataDoc.tags));
+ const chatTagsList = new Set<string>(StrListCast(this.View.dataDoc.tags_chat));
const facesList = new Set<string>(
- DocListCast(this._props.View.dataDoc[Doc.LayoutFieldKey(this._props.View.Document) + '_annotations'])
- .concat(this._props.View.Document)
+ DocListCast(this.View.dataDoc[Doc.LayoutFieldKey(this.View.Document) + '_annotations'])
+ .concat(this.View.Document)
.filter(d => d.face)
.map(doc => StrCast(DocCast(doc.face)?.title))
);
this._panelHeightDirty;
- return !this._props.View.Document.showTags ? null : (
+ return this.View.ComponentView?.isUnstyledView?.() || (!this.View.Document._layout_showTags && this._props.Views.length === 1) ? null : (
<div
className="tagsView-container"
- ref={r => r && new ResizeObserver(action(() => (this._props.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)}
+ ref={r => r && new ResizeObserver(action(() => this._props.Views.length === 1 && (this.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)}
style={{
transformOrigin: 'top left',
maxWidth: `${100 * this.currentScale}%`,
@@ -338,7 +348,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
position: 'relative',
- top: `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
+ top: this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
}}>
<div className="tagsView-content" style={{ width: '100%' }}>
<div className="tagsView-list">
@@ -346,10 +356,17 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
<IconButton style={{ width: '8px' }} tooltip="Close Menu" onPointerDown={() => this.setToEditing(!this._isEditing)} icon={<FontAwesomeIcon icon={this._isEditing ? 'chevron-up' : 'chevron-down'} size="sm" />} />
)}
{Array.from(tagsList).map((tag, i) => (
- <TagItem key={i} doc={this._props.View.Document} tag={tag} tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
+ <TagItem
+ key={i}
+ docs={this._props.Views.map(view => view.Document)}
+ tag={tag}
+ tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)}
+ setToEditing={this.setToEditing}
+ showRemoveUI={this.isEditing}
+ />
))}
{Array.from(facesList).map((tag, i) => (
- <TagItem key={i} doc={this._props.View.Document} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
+ <TagItem key={i} docs={this._props.Views.map(view => view.Document)} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
))}
</div>
{this.isEditing ? (
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index c799eb3c8..54cc02825 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -78,6 +78,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
NativeWidth={returnZero}
NativeHeight={returnZero}
fitWidth={undefined}
+ containerViewPath={this.childContainerViewPath}
onDoubleClickScript={this.onChildDoubleClick}
renderDepth={this._props.renderDepth + 1}
LayoutTemplate={this._props.childLayoutTemplate}
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index b402a7a32..c40f471d6 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -23,6 +23,21 @@
}
}
+.collectionCarouselView-addFlashcards {
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ z-index: -1;
+ pointer-events: none;
+}
+.collectionCarouselView-recentlyMissed {
+ color: red;
+ z-index: 999;
+ position: relative;
+ left: 10px;
+ top: 10px;
+ pointer-events: none;
+}
.carouselView-back,
.carouselView-fwd,
.carouselView-star,
@@ -68,41 +83,52 @@
right: 52%;
}
.carouselView-quiz {
- position: absolute;
+ position: relative;
display: flex;
- top: 5px;
- right: 8px;
+ flex-direction: column;
+ height: 20px;
+ align-items: center;
+ margin: auto;
&:hover {
color: white;
}
}
.carouselView-practice {
- position: absolute;
+ position: relative;
display: flex;
- top: 22px;
- right: 8px;
+ flex-direction: column;
+ height: 20px;
+ align-items: center;
+ margin: auto;
&:hover {
color: white;
}
}
.carouselView-starFilter {
- position: absolute;
+ position: relative;
display: flex;
- top: 40px;
- right: 7px;
+ height: 20px;
+ align-items: center;
&:hover {
color: white;
}
}
+.carouselView-practiceModes {
+ width: 100%;
+ height: 40px;
+ display: flex;
+ flex-direction: column;
+}
.carouselView-menu {
position: absolute;
+ flex-direction: column;
+ align-items: center;
display: flex;
top: 2px;
right: 2px;
width: 30;
- height: 60;
border-radius: 5px;
color: rgba(255, 255, 255, 0.5);
background: rgba(0, 0, 0, 0.1);
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index a1c59d238..d9a99f47f 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -1,11 +1,10 @@
/* eslint-disable react/jsx-props-no-spreading */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { action, computed, makeObservable, observable, trace } from 'mobx';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
-import { emptyFunction } from '../../../Utils';
+import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
import { Doc, Opt } from '../../../fields/Doc';
import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -19,15 +18,12 @@ import './CollectionCarouselView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
enum cardMode {
- // PRACTICE = 'practice',
STAR = 'star',
- // QUIZ = 'quiz',
ALL = 'all',
}
enum practiceMode {
PRACTICE = 'practice',
QUIZ = 'quiz',
- NORMAL = 'normal',
}
enum practiceVal {
MISSED = 'missed',
@@ -36,24 +32,20 @@ enum practiceVal {
@observer
export class CollectionCarouselView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
- @observable private _practiceMessage: string | undefined;
- @observable private _filterMessage: string | undefined;
get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
- get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
+ get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
get starField() { return "star"; } // prettier-ignore
+ _fadeTimer: NodeJS.Timeout | undefined;
+
constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
- // this.setModes();
- this.layoutDoc.filterOp = cardMode.ALL;
- Doc.setDocFilter(this.Document, 'star', undefined, 'match');
- this.layoutDoc.practiceMode = practiceMode.NORMAL;
- this.layoutDoc._carousel_index = 0;
- this.carouselItems.forEach(item => { item.layout[this.practiceField] = undefined}); //prettier-ignore
- console.log(this.carouselItems.length);
}
+ @observable _last_index = this.carouselIndex;
+ @observable _last_opacity = 1;
+
componentWillUnmount() {
this._dropDisposer?.();
}
@@ -65,65 +57,42 @@ export class CollectionCarouselView extends CollectionSubView() {
}
};
- @computed get carouselItems() {
- this.childLayoutPairs.map(pair => {
- pair.layout.embedContainer = this.Document;
- });
- return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK);
+ @computed get practiceMode() {
+ return this.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this.layoutDoc.practiceMode) : '';
}
- @computed get marginX() {
- return NumCast(this.layoutDoc.caption_xMargin, 50);
+ @computed get practiceMessage() {
+ const cardCount = this.carouselItems.length;
+ if (this.practiceMode) {
+ if (!Doc.hasDocFilter(this.layoutDoc, 'star') && !cardCount) {
+ return 'Finished! Click here to view all flashcards.';
+ }
+ }
+ return '';
}
- @action setPracticeMessage = (mes: string | undefined) => {
- this._practiceMessage = mes;
- };
- @action setFilterMessage = (mes: string | undefined) => {
- this._filterMessage = mes;
- };
-
- setModes = () => {
- this.layoutDoc.filterOp = cardMode.ALL;
- Doc.setDocFilter(this.Document, 'data_star', undefined, 'match');
- this.layoutDoc.practiceMode = practiceMode.NORMAL;
- this.layoutDoc._carousel_index = 0;
- };
-
- move = (dir: number) => {
- const moveToCardWithField = (match: (doc: Doc) => boolean): boolean => {
- let startInd = (NumCast(this.layoutDoc._carousel_index) + dir) % this.carouselItems.length;
- while (!match(this.carouselItems?.[startInd].layout) && (startInd + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) {
- startInd = (startInd + dir + this.carouselItems.length) % this.carouselItems.length;
+ @computed get filterMessage() {
+ const cardCount = this.carouselItems.length;
+ if (!this.practiceMessage) {
+ if (Doc.hasDocFilter(this.layoutDoc, 'star') && !cardCount) {
+ return 'No starred items. Click here to view all flash cards.';
}
- if (match(this.carouselItems?.[startInd].layout)) {
- this.layoutDoc._carousel_index = startInd;
- return true;
+ if (this.practiceMode) {
+ if (!cardCount) return 'No flashcards to show! Click here to leave practice mode';
}
- return match(this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout);
- };
-
- switch (this.layoutDoc.practiceMode && this.layoutDoc.filterOp) {
- case practiceMode.PRACTICE && cardMode.ALL:
- if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
- this._practiceMessage = 'Finished! Unselect practice mode to view all flashcards.';
- this.carouselItems.forEach(item => { item.layout[this.practiceField] = undefined}); //prettier-ignore
- }
- break;
- case !practiceMode.PRACTICE && cardMode.STAR:
- if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
- this._filterMessage = 'No starred items. Unselect this view to see all flashcards and star them.';
- }
- break;
- case practiceMode.PRACTICE && cardMode.STAR:
- if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT && doc[this.starField] === true)) {
- this._filterMessage = 'No flashcards to show! Unselect mode to view all flashcards.';
- this._practiceMessage = undefined;
- }
- break;
- default:
- moveToCardWithField(returnTrue);
}
- };
+ return '';
+ }
+ @computed get marginX() { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
+ @computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore
+ @computed get carouselItems() { return this.childDocs
+ .filter(doc => doc.type !== DocumentType.LINK)
+ .filter(doc => !this.practiceMode || (BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] !== practiceVal.CORRECT))// show only cards that aren't marked as correct
+ } // prettier-ignore
+
+ move = action((dir: number) => {
+ this._last_index = this.carouselIndex;
+ this.layoutDoc._carousel_index = this.carouselItems.length ? (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length : 0;
+ });
/**
* Goes to the next Doc in the stack subject to the currently selected filter option.
@@ -146,10 +115,8 @@ export class CollectionCarouselView extends CollectionSubView() {
*/
star = (e: React.MouseEvent) => {
e.stopPropagation();
- const curDoc = this.carouselItems[NumCast(this.layoutDoc._carousel_index)];
- curDoc.layout[this.starField] = curDoc.layout[this.starField] ? undefined : true;
- // if (!curDoc.layout[this.starField]) this.move(1);
- // this.layoutDoc._carousel_index = undefined;
+ const curDoc = this.carouselItems[this.carouselIndex];
+ curDoc && (curDoc[this.starField] = curDoc[this.starField] ? undefined : true);
};
/*
@@ -157,8 +124,8 @@ export class CollectionCarouselView extends CollectionSubView() {
*/
setPracticeVal = (e: React.MouseEvent, val: string) => {
e.stopPropagation();
- const curDoc = this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)];
- curDoc.layout[this.practiceField] = val;
+ const curDoc = this.carouselItems[this.carouselIndex];
+ curDoc && (curDoc[this.practiceField] = val);
this.advance(e);
};
@@ -172,71 +139,92 @@ export class CollectionCarouselView extends CollectionSubView() {
onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
- setPracticeMode = (mode: practiceMode) => {
+ setPracticeMode = (mode: practiceMode | undefined) => {
this.layoutDoc.practiceMode = mode;
- this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined));
- switch (mode) {
- case practiceMode.QUIZ:
- this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined));
- break;
- case practiceMode.NORMAL:
- this.setPracticeMessage(undefined);
- break;
- }
+ this.carouselItems?.map(doc => (doc[this.practiceField] = undefined));
+ if (mode === practiceMode.QUIZ) this.carouselItems?.map(doc => (doc[this.sideField] = undefined));
};
- setFilterMode = (mode: cardMode) => {
- this.layoutDoc.filterOp = mode;
- switch (mode) {
- case cardMode.STAR:
- // Doc.setDocFilter(this.Document, 'data_star', true, 'match');
- this.move(1);
- break;
- default:
- this.setFilterMessage(undefined); // prettier-ignore
- // Doc.setDocFilter(this.Document, 'data_star', true, 'remove');
- }
- };
contentScreentToLocalXf = () => this._props.ScreenToLocalTransform().translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin));
contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
+
+ isChildContentActive = () =>
+ this._props.isContentActive?.() === false
+ ? false
+ : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
+ ? true
+ : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
+ ? false
+ : undefined;
+
+ renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
+ return (
+ <DocumentView
+ {...this._props}
+ ref={overlayFunc}
+ Document={doc}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ fitWidth={undefined}
+ containerViewPath={this.childContainerViewPath}
+ setContentViewBox={undefined}
+ onDoubleClickScript={this.onContentDoubleClick}
+ onClickScript={this.onContentClick}
+ isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
+ isContentActive={this.isChildContentActive}
+ hideCaptions={showCaptions}
+ renderDepth={this._props.renderDepth + 1}
+ LayoutTemplate={this._props.childLayoutTemplate}
+ LayoutTemplateString={this._props.childLayoutString}
+ TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)}
+ childFilters={this.childDocFilters}
+ hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
+ addDocument={this._props.addDocument}
+ ScreenToLocalTransform={this.contentScreentToLocalXf}
+ PanelWidth={this.contentPanelWidth}
+ PanelHeight={this.contentPanelHeight}
+ />
+ );
+ };
+ /**
+ * Display an overlay of the previous card that crossfades to the next card
+ */
+ @computed get overlay() {
+ const fadeTime = 500;
+ const lastDoc = this.carouselItems?.[this._last_index];
+ return !lastDoc || this.carouselIndex === this._last_index ? null : (
+ <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, position: 'absolute', top: 0, left: 0, transition: `opacity ${fadeTime}ms` }}>
+ {this.renderDoc(
+ lastDoc,
+ false, // hide captions if the carousel is configured to show the captions
+ action((r: DocumentView | null) => {
+ if (r) {
+ this._fadeTimer && clearTimeout(this._fadeTimer);
+ this._last_opacity = 0;
+ this._fadeTimer = setTimeout(
+ action(() => {
+ this._last_index = -1;
+ this._last_opacity = 1;
+ }),
+ fadeTime
+ );
+ }
+ })
+ )}
+ </div>
+ );
+ }
@computed get content() {
- trace();
- if (this.layoutDoc._carousel_index === this.carouselItems.length && this.layoutDoc._carousel_index !== 0) {
- this.move(1);
- }
- const index = NumCast(this.layoutDoc._carousel_index);
+ const index = this.carouselIndex;
const curDoc = this.carouselItems?.[index];
const captionProps = { ...this._props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined };
const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption);
-
- return !(curDoc?.layout instanceof Doc) ? null : (
+ return !curDoc ? null : (
<>
- <div className="collectionCarouselView-image" style={{ padding: `${NumCast(this.layoutDoc.yMargin)}px ${NumCast(this.layoutDoc.xMargin)}px ${NumCast(this.layoutDoc.yMargin)}px ${NumCast(this.layoutDoc.xMargin)}px` }}>
- <DocumentView
- {...this._props}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- fitWidth={undefined}
- setContentViewBox={undefined}
- childFilters={this.childDocFilters}
- containerViewPath={this._props.docViewPath}
- onDoubleClickScript={this.onContentDoubleClick}
- onClickScript={this.onContentClick}
- hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
- isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
- isContentActive={(this._props.childContentsActive ?? this._props.isContentActive() === false) ? returnFalse : emptyFunction}
- addDocument={this._props.addDocument}
- hideCaptions={!!carouselShowsCaptions} // hide captions if the carousel is configured to show the captions
- renderDepth={this._props.renderDepth + 1}
- LayoutTemplate={this._props.childLayoutTemplate}
- LayoutTemplateString={this._props.childLayoutString}
- Document={curDoc.layout}
- TemplateDataDocument={DocCast(curDoc.layout.resolvedDataDoc)}
- ScreenToLocalTransform={this.contentScreentToLocalXf}
- PanelWidth={this.contentPanelWidth}
- PanelHeight={this.contentPanelHeight}
- />
+ <div className="collectionCarouselView-image" key="image">
+ {this.renderDoc(curDoc, !!carouselShowsCaptions)}
+ {this.overlay}
</div>
{!carouselShowsCaptions ? null : (
<div
@@ -249,17 +237,13 @@ export class CollectionCarouselView extends CollectionSubView() {
marginLeft: this.marginX,
width: `calc(100% - ${this.marginX * 2}px)`,
}}>
- <FormattedTextBox key={index} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc.layout} TemplateDataDocument={undefined} />
+ <FormattedTextBox key={index} xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc} TemplateDataDocument={undefined} />
</div>
)}
</>
);
}
- containsDifTypes = (): boolean => {
- return this.carouselItems.filter(doc => !doc.layout._layout_isFlashcard).length !== 0;
- };
-
addFlashcard() {
const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
this.addDocument?.(newDoc);
@@ -275,30 +259,28 @@ export class CollectionCarouselView extends CollectionSubView() {
}
@computed get buttons() {
- if (!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]) return null;
+ if (!this.carouselItems?.[this.carouselIndex]) return null;
return (
<>
+ <div>
+ <Tooltip title="star">
+ <div key="star" className="carouselView-star" onClick={this.star}>
+ <FontAwesomeIcon icon="star" color={this.carouselItems[this.carouselIndex]?.[this.starField] ? 'yellow' : 'gray'} size="1x" />
+ </div>
+ </Tooltip>
+ {/* <Tooltip title="add new flashcard to pile">
+ <div key="add" className="carouselView-add" onClick={this.addFlashcard}>
+ <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
+ </div>
+ </Tooltip> */}
+ </div>
<div key="back" className="carouselView-back" onClick={this.goback}>
<FontAwesomeIcon icon="chevron-left" size="2x" />
</div>
<div key="fwd" className="carouselView-fwd" onClick={this.advance}>
<FontAwesomeIcon icon="chevron-right" size="2x" />
</div>
- {!this.containsDifTypes() ? (
- <div>
- <Tooltip title="star">
- <div key="star" className="carouselView-star" onClick={this.star}>
- <FontAwesomeIcon icon="star" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
- </div>
- </Tooltip>
- {/* <Tooltip title="add new flashcard to pile">
- <div key="add" className="carouselView-add" onClick={this.addFlashcard}>
- <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
- </div>
- </Tooltip> */}
- </div>
- ) : null}
- {this.layoutDoc.practiceMode === practiceMode.PRACTICE ? (
+ {this.practiceMode ? (
<div>
<Tooltip title="Incorrect. View again later.">
<div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}>
@@ -316,33 +298,31 @@ export class CollectionCarouselView extends CollectionSubView() {
);
}
- togglePracticeMode = (mode: practiceMode) => {
- if (mode === this.layoutDoc.practiceMode) {
- this.setPracticeMode(practiceMode.NORMAL);
- // this.setPracticeMessage("undefined");
- } else this.setPracticeMode(mode);
- };
- toggleFilterMode = () => { this.layoutDoc.filterOp === cardMode.STAR ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.STAR)}; //prettier-ignore
+ togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.layoutDoc.practiceMode ? undefined : mode);
+ toggleFilterMode = () => Doc.setDocFilter(this.Document, 'star', true, 'match', true);
setColor = (mode: practiceMode | cardMode, which: string) => { return which === mode ? 'white' : 'light gray'}; //prettier-ignore
@computed get menu() {
+ const curDoc = this.carouselItems?.[this.carouselIndex];
return (
<div className="carouselView-menu">
- <Tooltip title="Practice flashcards using GPT">
- <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}>
- <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.layoutDoc.practiceMode))} size="1x" />
- </div>
- </Tooltip>
- <Tooltip title={this.layoutDoc.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
- <div key="back" className="carouselView-practice" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}>
- <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.layoutDoc.practiceMode))} size="1x" />
- </div>
- </Tooltip>
- <Tooltip title={this.layoutDoc.filterOp === cardMode.STAR ? 'Show all cards' : 'Show only starred cards'}>
+ <Tooltip title={Doc.hasDocFilter(this.Document, 'star') ? 'Show all cards' : 'Show only starred cards'}>
<div key="back" className="carouselView-starFilter" onClick={e => this.toggleFilterMode()}>
- <FontAwesomeIcon icon="filter" color={this.setColor(cardMode.STAR, StrCast(this.layoutDoc.filterOp))} size="1x" />
+ <FontAwesomeIcon icon="filter" color={Doc.hasDocFilter(this.Document, 'star') ? 'white' : 'lightgray'} size="1x" />
</div>
</Tooltip>
+ <div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}>
+ <Tooltip title="Practice flashcards using GPT">
+ <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}>
+ <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.layoutDoc.practiceMode))} size="1x" />
+ </div>
+ </Tooltip>
+ <Tooltip title={this.layoutDoc.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
+ <div key="back" className="carouselView-practice" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}>
+ <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.layoutDoc.practiceMode))} size="1x" />
+ </div>
+ </Tooltip>
+ </div>
</div>
);
}
@@ -356,36 +336,25 @@ export class CollectionCarouselView extends CollectionSubView() {
background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
}}>
- {!this._practiceMessage && !this._filterMessage ? (
+ {!this.practiceMessage && !this.filterMessage ? (
this.content
) : (
- <p className="message">
- {this._filterMessage}
- {'\n'}
- {this._practiceMessage}
+ <p
+ className="message"
+ onClick={e => {
+ if (this.filterMessage) {
+ this.layoutDoc.practiceMode = undefined;
+ Doc.setDocFilter(this.layoutDoc, 'star', undefined, 'remove');
+ }
+ this.childDocs.forEach(item => {
+ item[this.practiceField] = undefined;
+ });
+ }}>
+ {this.filterMessage || this.practiceMessage}
</p>
)}
- {!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.practiceMode === practiceMode.PRACTICE ? <p className="message">Add flashcards </p> : null}
- <p
- className="missed-message"
- style={{
- color: 'red',
- fontWeight: 'bold',
- zIndex: '999',
- position: 'relative',
- left: '10px',
- top: '10px',
- width: '10px',
- display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]
- ? this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED && this.layoutDoc.practiceMode === practiceMode.PRACTICE && !this._practiceMessage
- ? 'block'
- : 'none'
- : 'none',
- }}>
- Recently missed!
- </p>
- {!this.containsDifTypes() && this.carouselItems.length !== 0 ? this.menu : null}
- {this.Document._chromeHidden || (!this._filterMessage && !this._practiceMessage) ? this.buttons : null}
+ {!this.Document._chromeHidden ? this.menu : null}
+ {!this.Document._chromeHidden ? this.buttons : null}
</div>
);
}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e0aa79c7b..028133a6e 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -31,6 +31,7 @@ import { ScriptingRepl } from '../ScriptingRepl';
import { UndoStack } from '../UndoStack';
import './CollectionDockingView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { TabHTMLElement } from './TabDocView';
@observer
export class CollectionDockingView extends CollectionSubView() {
@@ -544,7 +545,7 @@ export class CollectionDockingView extends CollectionSubView() {
tabCreated = (tab: { contentItem: { element: HTMLElement[] } }) => {
this.tabMap.add(tab);
// InitTab is added to the tab's HTMLElement in TabDocView
- const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as unknown as { InitTab?: (tab: object) => void };
+ const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as unknown as TabHTMLElement;
tabdocviewContent?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content)
};
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 31b6be927..f56ea9d76 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -57,6 +57,7 @@ interface TabMiniThumbProps {
miniLeft: () => number;
}
+export type TabHTMLElement = HTMLDivElement & { InitTab?: (tab: object) => void };
@observer
class TabMiniThumb extends React.Component<TabMiniThumbProps> {
render() {
@@ -193,8 +194,9 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
.filter(tv => tv._document)
.map(tv => tv._document!);
}
- _mainCont: HTMLDivElement | null = null;
+ _mainCont: TabHTMLElement | null = null;
_tabReaction: IReactionDisposer | undefined;
+ _lastSelection = 0; // time when view was last selected - used to re-select views that get invalidated when selected
/**
* Adds a document to the presentation view
@@ -273,19 +275,24 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
}
+ // Flag indicating that when a tab is activated, it should not select it's document.
+ // this is used by the link properties menu when it wants to display the link target without selecting the target (which would make the link property window go away since it would no longer be selected)
+ public static DontSelectOnActivate = 'dontSelectOnActivate';
+
+ public static IsSelected = (doc?: Doc) => {
+ return DocumentView.getViews(doc).some(dv => dv?.IsSelected);
+ };
+
static Activate = (tabDoc: Doc) => {
const tab = Array.from(CollectionDockingView.Instance?.tabMap ?? []).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue);
tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
return tab !== undefined;
};
- // static ActivateTabView(doc: Doc) {
- // const tabView = Array.from(TabDocView._allTabs).find(view => view._document === doc);
- // if (!tabView?._activated && tabView?._document) {
- // TabDocView.Activate(tabView?._document);
- // return tabView;
- // }
- // return undefined;
- // }
+
+ get stack() { return this._props.glContainer.parent.parent; } // prettier-ignore
+ get tab() { return this._props.glContainer.tab; } // prettier-ignore
+ get view() { return this._view; } // prettier-ignore
+
constructor(props: TabDocViewProps) {
super(props);
makeObservable(this);
@@ -299,37 +306,13 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
@observable _hovering = false;
@observable _isActive: boolean = false;
@observable _isAnyChildContentActive = false;
- public static IsSelected = (doc?: Doc) => {
- if (DocumentView.getViews(doc).some(dv => dv?.IsSelected)) {
- return true;
- }
- return false;
- };
- @computed get _isUserActivated() {
- return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive;
- }
- get _isContentActive() {
- return this._isUserActivated || this._hovering;
- }
@observable _document: Doc | undefined = undefined;
@observable _view: DocumentView | undefined = undefined;
+ @observable _forceInvalidateScreenToLocal = 0; // screentolocal is computed outside of react using a dom resize ovbserver. this hack allows the resize observer to trigger a react update
- @computed get layoutDoc() {
- return this._document && Doc.Layout(this._document);
- }
-
- get stack() {
- return this._props.glContainer.parent.parent;
- }
- get tab() {
- return this._props.glContainer.tab;
- }
- get view() {
- return this._view;
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- _lastTab: any;
- _lastView: DocumentView | undefined;
+ @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } // prettier-ignore
+ @computed get isUserActivated() { return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive; } // prettier-ignore
+ @computed get isContentActive() { return this.isUserActivated || this._hovering; } // prettier-ignore
@action
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -413,7 +396,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
color === variant
? DashColor(color)
.fade(
- this._isUserActivated
+ this.isUserActivated
? 0
: this._hovering
? 0.25
@@ -522,19 +505,14 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
this._props.glContainer.layoutManager.off('activeContentItemChanged', this.onActiveContentItemChanged);
}
- // Flag indicating that when a tab is activated, it should not select it's document.
- // this is used by the link properties menu when it wants to display the link target without selecting the target (which would make the link property window go away since it would no longer be selected)
- public static DontSelectOnActivate = 'dontSelectOnActivate';
-
- @action.bound
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- private onActiveContentItemChanged(contentItem: any) {
+ onActiveContentItemChanged = action((contentItem: any) => {
if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) {
this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab;
if (!this._view && this.tab?.contentItem?.config?.props?.panelName !== TabDocView.DontSelectOnActivate) setTimeout(() => DocumentView.SelectView(this._view, false));
!this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one.
}
- }
+ });
// adds a tab to the layout based on the locaiton parameter which can be:
// close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab,
@@ -551,7 +529,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
const panelName = whereFields.length > 1 ? whereFields.lastElement() : '';
if (docs[0]?.dockingConfig && !keyValue) return DashboardView.openDashboard(docs[0]);
- // prettier-ignore
switch (whereFields[0]) {
case undefined:
case OpenWhere.lightbox: return LightboxView.Instance.AddDocTab(docs[0], location);
@@ -559,7 +536,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
case OpenWhere.replace: return CollectionDockingView.ReplaceTab(docs[0], whereMods, this.stack, panelName, undefined, keyValue);
case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(docs[0], whereMods, this.stack, TabDocView.DontSelectOnActivate, keyValue);
case OpenWhere.add:default:return CollectionDockingView.AddSplit(docs[0], whereMods, this.stack, undefined, keyValue);
- }
+ } // prettier-ignore
};
remDocTab = (doc: Doc | Doc[]) => {
if (doc === this._document) {
@@ -571,8 +548,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
};
getCurrentFrame = () => NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame);
-
- @action
focusFunc = () => {
if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) {
this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
@@ -580,7 +555,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
return undefined;
};
active = () => this._isActive;
- @observable _forceInvalidateScreenToLocal = 0;
ScreenToLocalTransform = () => {
this._forceInvalidateScreenToLocal;
const { translateX, translateY } = ClientUtils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement);
@@ -594,47 +568,44 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
whenChildContentActiveChanges = (isActive: boolean) => {
this._isAnyChildContentActive = isActive;
};
- isContentActive = () => this._isContentActive;
+ isContentActiveFunc = () => this.isContentActive;
waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined);
- @computed get docView() {
- return !this._activated || !this._document ? null : (
- <>
- <DocumentView
- key={this._document[Id]}
- ref={action((r: DocumentView) => {
- this._lastView && DocumentView.removeView(this._lastView);
- this._view = r;
- this._lastView = this._view;
- })}
- renderDepth={0}
- LayoutTemplateString={this._props.keyValue ? KeyValueBox.LayoutString() : undefined}
- hideTitle={this._props.keyValue}
- Document={this._document}
- TemplateDataDocument={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined}
- waitForDoubleClickToClick={this.waitForDoubleClick}
- isContentActive={this.isContentActive}
- isDocumentActive={returnFalse}
- PanelWidth={this.PanelWidth}
- PanelHeight={this.PanelHeight}
- styleProvider={DefaultStyleProvider}
- childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyFilter}
- childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyFilter}
- searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
- addDocument={undefined}
- removeDocument={this.remDocTab}
- addDocTab={this.addDocTab}
- suppressSetHeight={!!this._document._layout_fitWidth}
- ScreenToLocalTransform={this.ScreenToLocalTransform}
- dontCenter="y"
- whenChildContentsActiveChanged={this.whenChildContentActiveChanges}
- focus={this.focusFunc}
- containerViewPath={returnEmptyDocViewList}
- pinToPres={TabDocView.PinDoc}
- />
- {this.disableMinimap() ? null : <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />}
- </>
- );
- }
+ renderDocView = (doc: Doc) => (
+ <DocumentView
+ key={doc[Id]}
+ ref={action((r: DocumentView) => {
+ const now = Date.now();
+ this._lastSelection = this._view?.IsSelected ? now : this._lastSelection;
+ if (this._view) DocumentView.removeView(this._view);
+ this._view = r;
+ if (this._view && now - this._lastSelection < 1000) this._view.select(false);
+ })}
+ renderDepth={0}
+ LayoutTemplateString={this._props.keyValue ? KeyValueBox.LayoutString() : undefined}
+ hideTitle={this._props.keyValue}
+ Document={doc}
+ TemplateDataDocument={!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined}
+ waitForDoubleClickToClick={this.waitForDoubleClick}
+ isContentActive={this.isContentActiveFunc}
+ isDocumentActive={returnFalse}
+ PanelWidth={this.PanelWidth}
+ PanelHeight={this.PanelHeight}
+ styleProvider={DefaultStyleProvider}
+ childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyFilter}
+ childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyFilter}
+ searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
+ addDocument={undefined}
+ removeDocument={this.remDocTab}
+ addDocTab={this.addDocTab}
+ suppressSetHeight={!!doc._layout_fitWidth}
+ ScreenToLocalTransform={this.ScreenToLocalTransform}
+ dontCenter="y"
+ whenChildContentsActiveChanged={this.whenChildContentActiveChanges}
+ focus={this.focusFunc}
+ containerViewPath={returnEmptyDocViewList}
+ pinToPres={TabDocView.PinDoc}
+ />
+ );
render() {
return (
@@ -647,23 +618,21 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
onPointerLeave={action(() => { this._hovering = false; })} // prettier-ignore
onDragOver={action(() => { this._hovering = true; })} // prettier-ignore
onDragLeave={action(() => { this._hovering = false; })} // prettier-ignore
- ref={ref => {
+ ref={(ref: TabHTMLElement) => {
+ // "add" an InitTab function to this div to call from tabCreated in CollectionDockingView when div is reused
this._mainCont = ref;
if (this._mainCont) {
- if (this._lastTab) {
- this._view && DocumentView.removeView(this._view);
- }
- this._lastTab = this.tab;
- (this._mainCont as { InitTab?: (tab: object) => void }).InitTab = (tab: object) => this.init(tab, this._document);
- DocServer.GetRefField(this._props.documentId).then(
- action(doc => {
- doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document);
- })
- );
- ref && new ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(ref);
+ this._mainCont.InitTab = (tab: object) => this.init(tab, this._document);
+ DocServer.GetRefField(this._props.documentId).then(action(doc => {
+ doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document);
+ })); // prettier-ignore
+ new ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(this._mainCont);
}
}}>
- {this.docView}
+ {!this._activated || !this._document ? null : this.renderDocView(this._document)}
+ {this.disableMinimap() || !this._document ? null : (
+ <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />
+ )}
</div>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index f9f6c81ab..534f67927 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -179,7 +179,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
})}>
{FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => {
- const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.');
+ const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url.href.split('.') ?? ['-missing-', '.png'];
return (
<div
className="image-wrapper"
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
index 94ec59ecb..753685b97 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
@@ -139,9 +139,9 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
toggleDisplayInformation = () => {
this._displayImageInformation = !this._displayImageInformation;
if (this._displayImageInformation) {
- this._selectedImages.forEach(doc => (doc[DocData].showTags = true));
+ this._selectedImages.forEach(doc => (doc._layout_showTags = true));
} else {
- this._selectedImages.forEach(doc => (doc[DocData].showTags = false));
+ this._selectedImages.forEach(doc => (doc._layout_showTags = false));
}
};
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 738f6d699..3ec799836 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -1,6 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { lift, toggleMark, wrapIn } from 'prosemirror-commands';
import { Mark, MarkType } from 'prosemirror-model';
@@ -68,7 +68,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
constructor(props: AntimodeMenuProps) {
super(props);
makeObservable(this);
- RichTextMenu._instance.menu = this;
+ runInAction(() => (RichTextMenu._instance.menu = this));
this.updateMenu(undefined, undefined, props, this.layoutDoc);
this._canFade = false;
this.Pinned = true;
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index e0d6c7c05..0ef67b4be 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -404,7 +404,7 @@ export class RichTextRules {
if (!tags.includes(tag)) {
tags.push(tag);
this.Document[DocData].tags = new List<string>(tags);
- this.Document[DocData].showTags = true;
+ this.Document._layout_showTags = true;
}
const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag });
return state.tr
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index 4f6f5d314..c507e54b6 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -119,8 +119,7 @@ export class FaceRecognitionHandler {
* @param faceDoc - unique face Doc
*/
public static UniqueFaceRemoveFaceImage = (faceAnno: Doc, faceDoc: Doc) => {
- Doc.RemoveDocFromList(faceDoc[DocData], 'face_annos', faceAnno);
- faceAnno.face = undefined;
+ FaceRecognitionHandler.ImageDocFaceAnnos(faceAnno).forEach(face => Doc.RemoveDocFromList(faceDoc[DocData], 'face_annos', face) && (face.face = undefined));
};
constructor() {
@@ -221,6 +220,7 @@ export class FaceRecognitionHandler {
.then(imgDocFaceDescriptions => { // For each face detected, find a match.
const annos = [] as Doc[];
const scale = NumCast(imgDoc.data_nativeWidth) / img.width;
+ const showTags= imgDocFaceDescriptions.length > 1;
imgDocFaceDescriptions.forEach((fd, i) => {
const faceDescriptor = new List<number>(Array.from(fd.descriptor));
const matchedUniqueFace = this.findMatchingFaceDoc(fd.descriptor) ?? this.createUniqueFaceDoc(activeDashboard);
@@ -234,12 +234,14 @@ export class FaceRecognitionHandler {
y: fd.alignedRect.box.top * scale,
_width: fd.alignedRect.box.width * scale,
_height: fd.alignedRect.box.height * scale,
+ _layout_showTags: showTags
})
FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, matchedUniqueFace); // add image/faceDescriptor to matched unique face
annos.push(faceAnno);
});
imgDoc[DocData].data_annotations = new List<Doc>(annos);
+ imgDoc._layout_showTags = annos.length > 0;
return imgDocFaceDescriptions;
})
); // prettier-ignore
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 7abba7679..29903cfb5 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -24,6 +24,7 @@ import { listSpec } from './Schema';
import { ComputedField, ScriptField } from './ScriptField';
import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor, toList } from './Types';
import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util';
+import { KEY } from 'google-maps';
export let ObjGetRefField: (id: string, force?: boolean) => Promise<Doc | undefined>;
export let ObjGetRefFields: (ids: string[]) => Promise<Map<string, Doc | undefined>>;
@@ -1360,6 +1361,13 @@ export namespace Doc {
export const FilterAny = '--any--';
export const FilterNone = '--undefined--';
+ export function hasDocFilter(container: Opt<Doc>, key: string, fieldPrefix?: string) {
+ if (!container) return;
+ const filterField = '_' + (fieldPrefix ? fieldPrefix + '_' : '') + 'childFilters';
+ const childFilters = StrListCast(container[filterField]);
+ return childFilters.some(filter => filter.split(FilterSep)[0] === key);
+ }
+
// 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