aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/CollectionCarouselView.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/collections/CollectionCarouselView.tsx')
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx232
1 files changed, 138 insertions, 94 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 264a34680..9741c45fe 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -1,16 +1,15 @@
-/* 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 { Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
import { ContextMenu } from '../ContextMenu';
import { StyleProp } from '../StyleProp';
+import { TagItem } from '../TagsView';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
@@ -30,15 +29,36 @@ enum practiceVal {
export class CollectionCarouselView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
- get starField() { return this.fieldKey + "_star"; } // 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,11 @@ 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];
+ if (curDoc) {
+ if (TagItem.docHasTag(curDoc, this.starField)) TagItem.removeTagFromDoc(curDoc, this.starField);
+ else TagItem.addTagToDoc(curDoc, this.starField);
+ }
};
/*
@@ -116,8 +120,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 +136,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
@@ -142,34 +145,78 @@ export class CollectionCarouselView extends CollectionSubView() {
!revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
};
childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null));
+
+ 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={returnTrue}
- setContentViewBox={undefined}
- containerViewPath={this._props.docViewPath}
- 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}
- PanelWidth={this._props.PanelWidth}
- />
+ {this.renderDoc(curDoc, !!carouselShowsCaptions)}
+ {this.overlay}
</div>
{!carouselShowsCaptions ? null : (
<div
@@ -182,14 +229,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}>
@@ -199,7 +246,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={TagItem.docHasTag(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" />
@@ -211,6 +258,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
@@ -222,29 +287,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>
);