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.tsx243
1 files changed, 102 insertions, 141 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 8b3a699ed..a7d217076 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -1,64 +1,41 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
-import { ContextMenu } from '../ContextMenu';
+import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
-import { TagItem } from '../TagsView';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import './CollectionCarouselView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-enum cardMode {
- PRACTICE = 'practice',
- STAR = 'star',
- QUIZ = 'quiz',
-}
-enum practiceVal {
- MISSED = 'missed',
- CORRECT = 'correct',
-}
@observer
export class CollectionCarouselView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
- get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
- get starField() { return "#star"; } // prettier-ignore
+ _oldWheel: HTMLElement | null = null;
_fadeTimer: NodeJS.Timeout | undefined;
- _resetter: IReactionDisposer | undefined;
+ @observable _last_index = this.carouselIndex;
+ @observable _last_opacity = 1;
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
- );
+ componentDidMount(): void {
+ this._props.setContentViewBox?.(this);
}
componentWillUnmount() {
this._dropDisposer?.();
- this._resetter?.();
}
protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
@@ -66,96 +43,78 @@ export class CollectionCarouselView extends CollectionSubView() {
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
}
+ this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+ this._oldWheel = ele;
+ // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+ ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
};
- @computed get marginX() { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
+ @computed get captionMarginX(){ 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 => {
- 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
- });
- }
+ @computed get carouselItems() { return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg); } // prettier-ignore
+ /**
+ * Move forward or backward the specified number of Docs
+ * @param dir signed number indicating Docs to move forward or backward
+ */
move = action((dir: number) => {
this._last_index = this.carouselIndex;
- this.layoutDoc._carousel_index = (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length;
+ 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.
*/
- advance = (e: React.MouseEvent) => {
- e.stopPropagation();
- this.move(1);
- };
+ advance = () => this.move(1);
/**
* Goes to the previous Doc in the stack subject to the currently selected filter option.
*/
- goback = (e: React.MouseEvent) => {
- e.stopPropagation();
- this.move(-1);
- };
+ goback = () => this.move(-1);
- /*
- * Stars the document when the star button is pressed.
- */
- star = (e: React.MouseEvent) => {
- e.stopPropagation();
- 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);
+ curDoc = () => this.carouselItems[this.carouselIndex]?.layout;
+
+ focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
+ const docs = DocListCast(this.Document[this.fieldKey]);
+ if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+ const newIndex = anchor.config_carousel_index ?? docs.getIndex(DocCast(anchor.annotationOn, anchor));
+ options.didMove = newIndex !== this.layoutDoc._carousel_index;
+ options.didMove && (this.layoutDoc._carousel_index = newIndex);
}
+ return undefined;
};
- /*
- * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode.
- */
- setPracticeVal = (e: React.MouseEvent, val: string) => {
- e.stopPropagation();
- const curDoc = this.carouselItems[this.carouselIndex];
- curDoc && (curDoc[this.practiceField] = val);
- this.advance(e);
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.carouselIndex });
+ PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document);
+ addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+ return anchor;
};
+ addDocTab = this.addLinkedDocTab;
captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
// first look for properties on the document in the carousel, then fallback to properties on the container
const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property);
};
- panelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0);
+ contentPanelWidth = () => (this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin)) / this.nativeScaling();
+ contentPanelHeight = () => (this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin)) / this.nativeScaling();
onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
- 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
- revealItems.push({description: 'Star', event: () => {this.layoutDoc.filterOp = cardMode.STAR;}, icon: 'star',}); // prettier-ignore
- revealItems.push({description: 'Practice Mode', event: () => {this.layoutDoc.filterOp = cardMode.PRACTICE;}, icon: 'check',}); // prettier-ignore
- 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' });
- };
-
+ captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX;
+ contentScreenToLocalXf = () =>
+ this._props
+ .ScreenToLocalTransform() //
+ .translate(-NumCast(this.layoutDoc.xMargin) / this.nativeScaling(), -NumCast(this.layoutDoc.yMargin) / this.nativeScaling());
isChildContentActive = () =>
this._props.isContentActive?.() === false
? false
- : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
+ : this._props.isContentActive()
? true
: this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
? false
- : undefined;
-
- childScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
-
+ : undefined; // prettier-ignore
+ onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
return (
<DocumentView
@@ -165,10 +124,10 @@ export class CollectionCarouselView extends CollectionSubView() {
NativeWidth={returnZero}
NativeHeight={returnZero}
fitWidth={this._props.childLayoutFitWidth}
+ hideFilterStatus={true}
showTags={BoolCast(this.layoutDoc.showChildTags)}
containerViewPath={this.childContainerViewPath}
setContentViewBox={undefined}
- ScreenToLocalTransform={this.childScreenToLocalXf}
onDoubleClickScript={this.onContentDoubleClick}
onClickScript={this.onContentClick}
isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
@@ -178,8 +137,14 @@ export class CollectionCarouselView extends CollectionSubView() {
LayoutTemplate={this._props.childLayoutTemplate}
LayoutTemplateString={this._props.childLayoutString}
TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)}
- xPadding={35}
- PanelHeight={this.panelHeight}
+ childFilters={this.childDocFilters}
+ focus={this.focus}
+ hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
+ addDocument={this._props.addDocument}
+ ScreenToLocalTransform={this.contentScreenToLocalXf}
+ PanelWidth={this.contentPanelWidth}
+ PanelHeight={this.contentPanelHeight}
+ screenXPadding={this.screenXPadding}
/>
);
};
@@ -188,9 +153,9 @@ export class CollectionCarouselView extends CollectionSubView() {
*/
@computed get overlay() {
const fadeTime = 500;
- const lastDoc = this.carouselItems?.[this._last_index];
+ const lastDoc = this.carouselItems?.[this._last_index]?.layout;
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` }}>
+ <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, transition: `opacity ${fadeTime}ms` }}>
{this.renderDoc(
lastDoc,
false, // hide captions if the carousel is configured to show the captions
@@ -211,15 +176,18 @@ export class CollectionCarouselView extends CollectionSubView() {
</div>
);
}
+ @computed get renderedDoc() {
+ const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption);
+ return this.renderDoc(this.curDoc(), !!carouselShowsCaptions);
+ }
+
@computed get content() {
- 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 ? null : (
+ return !this.curDoc() ? null : (
<>
<div className="collectionCarouselView-image" key="image">
- {this.renderDoc(curDoc, !!carouselShowsCaptions)}
+ {this.renderedDoc}
{this.overlay}
</div>
{!carouselShowsCaptions ? null : (
@@ -229,68 +197,61 @@ export class CollectionCarouselView extends CollectionSubView() {
onWheel={StopEvent}
style={{
borderRadius: this._props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding) as string,
- marginRight: this.marginX,
- marginLeft: this.marginX,
- width: `calc(100% - ${this.marginX * 2}px)`,
+ marginRight: this.captionMarginX,
+ marginLeft: this.captionMarginX,
+ width: `calc(100% - ${this.captionMarginX * 2}px)`,
}}>
- <FormattedTextBox key={index} xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc} TemplateDataDocument={undefined} />
+ <FormattedTextBox xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={this.curDoc()} TemplateDataDocument={undefined} />
</div>
)}
</>
);
}
- @computed get buttons() {
- if (!this.carouselItems?.[this.carouselIndex]) return null;
- return (
+
+ @computed get navButtons() {
+ return !this.curDoc() ? null : (
<>
- <div key="back" className="carouselView-back" onClick={this.goback}>
+ <div key="back" className="carouselView-back" style={{ transform: `scale(${this.uiBtnScaling})` }} onClick={this.goback}>
<FontAwesomeIcon icon="chevron-left" size="2x" />
</div>
- <div key="fwd" className="carouselView-fwd" onClick={this.advance}>
+ <div key="fwd" className="carouselView-fwd" style={{ transform: `scale(${this.uiBtnScaling})` }} onClick={this.advance}>
<FontAwesomeIcon icon="chevron-right" size="2x" />
</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" />
- </div>
- <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}>
- <FontAwesomeIcon icon="check" color="green" size="1x" />
- </div>
</>
);
}
- /**
- * 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
+ nativeScaling = () => this._props.NativeDimScaling?.() || 1;
- /**
- * 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
+ docViewProps = () => ({
+ ...this._props, //
+ isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
+ isContentActive: this.isChildContentActive,
+ ScreenToLocalTransform: this.contentScreenToLocalXf,
+ });
+ answered = (correct: boolean) => (!correct || !this.curDoc() || NumCast(this.layoutDoc._carousel_index) === this.carouselItems.length - 1) && this.advance();
render() {
return (
- <div
- className="collectionCarouselView-outer"
- ref={this.createDashEventsTarget}
- onContextMenu={this.specificMenu}
- style={{
- 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.content}
- {this.renderAddFlashcards()}
- {this.renderRecentlyMissed()}
- {this.Document._chromeHidden ? null : this.buttons}
+ <div>
+ <div
+ className="collectionCarouselView-outer"
+ ref={this.createDashEventsTarget}
+ style={{
+ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
+ color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
+ left: NumCast(this.layoutDoc._xMargin),
+ top: NumCast(this.layoutDoc._yMargin),
+ transformOrigin: 'top left',
+ transform: `scale(${this.nativeScaling()})`,
+ width: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._xMargin)) / this.nativeScaling()}px)`,
+ height: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._yMargin)) / this.nativeScaling()}px)`,
+ position: 'relative',
+ }}>
+ {this.content}
+ {this.navButtons}
+ </div>
+ {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
</div>
);
}