aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx1
-rw-r--r--src/client/views/collections/CollectionCarouselView.scss15
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx223
-rw-r--r--src/client/views/collections/TabDocView.tsx4
4 files changed, 151 insertions, 92 deletions
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 f115bb40a..01b20d6d3 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -12,6 +12,21 @@
user-select: none;
}
}
+.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,
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 2f8d5adaf..ad868fd1e 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -1,12 +1,11 @@
/* eslint-disable react/jsx-props-no-spreading */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { computed, makeObservable } 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 { Doc, Opt } from '../../../fields/Doc';
-import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
import { ContextMenu } from '../ContextMenu';
@@ -32,13 +31,34 @@ export class CollectionCarouselView extends CollectionSubView() {
get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
get starField() { return "star"; } // prettier-ignore
+ _fadeTimer: NodeJS.Timeout | undefined;
+ _resetter: IReactionDisposer | undefined;
+
constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
}
+ @observable _last_index = this.carouselIndex;
+ @observable _last_opacity = 1;
+
+ componentDidMount() {
+ this._resetter = reaction(
+ // automatically reset practice fields when all cards have been marked as correct
+ () => this.carouselItems.length,
+ itemsCount => {
+ if (this.layoutDoc.filterOp === cardMode.PRACTICE && !itemsCount) {
+ this.layoutDoc.filterOp = undefined; // if all of the cards are correct, show all cards and exit practice mode
+ this.carouselItems.forEach(item => { // reset all the practice values
+ item[this.practiceField] = undefined;
+ });
+ }
+ } // prettier-ignore
+ );
+ }
componentWillUnmount() {
this._dropDisposer?.();
+ this._resetter?.();
}
protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
@@ -48,43 +68,24 @@ export class CollectionCarouselView extends CollectionSubView() {
}
};
+ @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.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK);
- }
- @computed get marginX() {
- return NumCast(this.layoutDoc.caption_xMargin, 50);
+ return DocListCast(this.childDocList)
+ .filter(doc => doc.type !== DocumentType.LINK)
+ .filter(doc => {
+ switch (StrCast(this.layoutDoc.filterOp)) {
+ case cardMode.STAR: return !!doc[this.starField]; // show only cards that are starred
+ case cardMode.PRACTICE: return doc[this.practiceField] !== practiceVal.CORRECT;// show only cards that aren't marked as correct
+ default: return true;
+ } // prettier-ignore
+ });
}
- 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 + dir + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) {
- startInd = (startInd + dir + this.carouselItems.length) % this.carouselItems.length;
- }
- if (match(this.carouselItems?.[startInd].layout)) {
- this.layoutDoc._carousel_index = startInd;
- return true;
- }
- return match(this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout);
- };
- switch (StrCast(this.layoutDoc.filterOp)) {
- case cardMode.STAR: // go to a flashcard that is starred, skip the ones that aren't
- if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
- this.layoutDoc.filterOp = undefined; // if there aren't any starred, show all cards
- }
- break;
- case cardMode.PRACTICE: // go to a new index that is missed, skip the ones that are correct
- if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
- this.layoutDoc.filterOp = undefined; // if all of the cards are correct, show all cards and exit practice mode
-
- this.carouselItems.forEach(item => { // reset all the practice values
- item.layout[this.practiceField] = undefined;
- });
- }
- break;
- default: moveToCardWithField(returnTrue);
- } // prettier-ignore
- };
+ move = action((dir: number) => {
+ this._last_index = this.carouselIndex;
+ this.layoutDoc._carousel_index = (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length;
+ });
/**
* Goes to the next Doc in the stack subject to the currently selected filter option.
@@ -107,8 +108,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;
+ const curDoc = this.carouselItems[this.carouselIndex];
+ curDoc && (curDoc[this.starField] = curDoc[this.starField] ? undefined : true);
};
/*
@@ -116,8 +117,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);
};
@@ -132,7 +133,6 @@ export class CollectionCarouselView extends CollectionSubView() {
captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
specificMenu = (): void => {
const cm = ContextMenu.Instance;
-
const revealOptions = cm.findByDescription('Filter Flashcards');
const revealItems = revealOptions?.subitems ?? [];
revealItems.push({description: 'All', event: () => {this.layoutDoc.filterOp = undefined;}, icon: 'layer-group',}); // prettier-ignore
@@ -141,32 +141,78 @@ export class CollectionCarouselView extends CollectionSubView() {
revealItems.push({description: 'Quiz Cards', event: () => {this.layoutDoc.filterOp = cardMode.QUIZ;}, icon: 'pencil',}); // prettier-ignore
!revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
};
+
+ 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)}
+ PanelHeight={this.panelHeight}
+ />
+ );
+ };
+ /**
+ * 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() {
- 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" key="image">
- <DocumentView
- {...this._props}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- fitWidth={undefined}
- setContentViewBox={undefined}
- onDoubleClickScript={this.onContentDoubleClick}
- onClickScript={this.onContentClick}
- isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
- isContentActive={(this._props.childContentsActive ?? this._props.isContentActive() === false) ? returnFalse : emptyFunction}
- 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)}
- PanelHeight={this.panelHeight}
- />
+ {this.renderDoc(curDoc, !!carouselShowsCaptions)}
+ {this.overlay}
</div>
{!carouselShowsCaptions ? null : (
<div
@@ -179,14 +225,14 @@ 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>
)}
</>
);
}
@computed get buttons() {
- if (!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]) return null;
+ if (!this.carouselItems?.[this.carouselIndex]) return null;
return (
<>
<div key="back" className="carouselView-back" onClick={this.goback}>
@@ -196,7 +242,7 @@ export class CollectionCarouselView extends CollectionSubView() {
<FontAwesomeIcon icon="chevron-right" size="2x" />
</div>
<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" />
+ <FontAwesomeIcon icon="star" color={this.carouselItems?.[this.carouselIndex][this.starField] ? 'yellow' : 'gray'} size="1x" />
</div>
<div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}>
<FontAwesomeIcon icon="xmark" color="red" size="1x" />
@@ -208,6 +254,24 @@ export class CollectionCarouselView extends CollectionSubView() {
);
}
+ /**
+ * Prompts user to add more flashcaards if they are in practice mode but there are no flashcards
+ */
+ renderAddFlashcards = () => <p
+ className="collectionCarouselView-addFlashcards"
+ style={{display: !this.carouselItems?.[this.carouselIndex] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none'}}>
+ Add flashcards!
+ </p> // prettier-ignore
+
+ /**
+ * Displays message that a flashcard was recently missed if it had previously been marked as wrong.
+ * */
+ renderRecentlyMissed = () => <p
+ className="collectionCarouselView-recentlyMissed"
+ style={{display: this.carouselItems?.[this.carouselIndex]?.[this.practiceField] === practiceVal.MISSED ? 'block' : 'none'}}>
+ Recently missed!
+ </p> // prettier-ignore
+
render() {
return (
<div
@@ -219,29 +283,8 @@ export class CollectionCarouselView extends CollectionSubView() {
color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
}}>
{this.content}
- {/* Displays a message to the user to add more flashcards if they are in practice mode and no flashcards are there. */}
- <p
- style={{
- display: !this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none',
- justifyContent: 'center',
- alignItems: 'center',
- height: '100%',
- zIndex: '-1',
- }}>
- Add flashcards!
- </p>
- {/* Displays a message to the user that a flashcard was recently missed if they had previously gotten it wrong. */}
- <p
- style={{
- color: 'red',
- zIndex: '999',
- position: 'relative',
- left: '10px',
- top: '10px',
- display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] ? (this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED ? 'block' : 'none') : 'none',
- }}>
- Recently missed!
- </p>
+ {this.renderAddFlashcards()}
+ {this.renderRecentlyMissed()}
{this.Document._chromeHidden ? null : this.buttons}
</div>
);
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index def1ea731..f56ea9d76 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -506,13 +506,13 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- 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,