aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/ComparisonBox.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2024-10-14 19:55:32 -0400
committerbobzel <zzzman@gmail.com>2024-10-14 19:55:32 -0400
commit29b83f023442c313ca5cf95f70f6430f101060e6 (patch)
tree9847ff4419e50f5bc8b7c1512a07b72275cd3b40 /src/client/views/nodes/ComparisonBox.tsx
parenta60c12ddef3db4123dffb2c91b446d20633f523a (diff)
reorganized comparisonBox related components -- moved stuff down into Docs.Crete and CurrentUserUtils. changed Doc.Copy to copy Doc's in fields tagged with cloneOnCopy. Changed ComparisonBox to support hover for slide or flip views. Fixed pointerEfvents for hover in comparisonBox
Diffstat (limited to 'src/client/views/nodes/ComparisonBox.tsx')
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx196
1 files changed, 103 insertions, 93 deletions
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 80ef126dc..38ce5f2f7 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -35,22 +35,21 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox';
const API_URL = 'https://api.unsplash.com/search/photos';
/**
- * This view serves three distinct functions depending on the metadata field layout_isFlashcard
- * 1) it provides a before/after animated sliding transition between two Docs
- * 2) it provides a question/answer switch between two Docs (flashcard)
- * 3) it provides a quiz view that displays a question and a user answer that can be "scored" by GPT
+ * This view serves two distinct functions depending on the revealOp field ('slide' or 'flip)
+ * 1) ('slide') - provides a before/after animated sliding transition between two Docs
+ * 2) ('flip') - provides a question/answer flip between two Docs
+ * And a third function that overrides the first two if the doc's container has its 'practiceMode' set to 'quiz'
+ * 3) ('quiz') - it provides a quiz view that displays a question and a user answer that can be "scored" by GPT
+ * NOTE: this should probably be changed to passing down a prop to the flashcard telling it to render as a quiz.
*
* In each case, the two docs are stored in the <fieldKey>_front and <fieldKey>_back fields
*
- * In the case of the flashcard, there is an icon that allows the user to choose between a
- * hover and a flip action to switch between cards. The transition is stored in the 'revealOp' field.
- * In addition, if a flashcard is created without data in the front/back fields, this will
- * create Text documents with placeholder text indicating to the user how to fill in the cards.
- * One option is to allow the user to enter a topic and, by clicking on the flashcard stack button,
- * convert the comparision box into a stack of comparison boxes filled in by GPT about the topic.
+ * For 'flip' and 'slide', the trigger can either be clicking, or hovering as determined by the revealOp_hover field.
+ * For 'quiz' the data of both Docs are shown in a single-view quiz display.
*
- * Quiz mode is activated when the parent collection has its 'quiz' field set when it renders a flashcard.
- * NOTE: this should probably be changed to passing down a prop to the flashcard telling it to render as a quiz.
+ * Users can create a stack of flashcards all at once (only) from an empty flashcard by entering a topic into the front card
+ * and clicking on the flashcard stack button. This will convert the comparision box into a stack of comparison boxes
+ * filled in by GPT about the topic.
*
*/
@@ -67,7 +66,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
*/
public static createFlashcard(tuple: string, frontKey: string, backKey: string, useDoc?: Doc) {
const [ktoken, atoken] = [ComparisonBox.ktoken, ComparisonBox.atoken];
- const newDoc = useDoc ?? Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0];
const rest = tuple.replace(question, '');
// prettier-ignore
@@ -78,9 +76,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
rest.replace(atoken,""); // finally if there's no keyword, just get rid of answer token and take what's left
const keyword = rest.replace(atoken, '').replace(answer, '').replace(ktoken, '').trim();
const fillInFlashcard = (img?: Doc) => {
- newDoc[DocData][frontKey] = FormattedTextBox.centeredTextCreator('question', question, img);
- newDoc[DocData][backKey] = FormattedTextBox.centeredTextCreator('answer', answer);
- return newDoc;
+ const front = Docs.Create.CenteredTextCreator('question', question, {}, img);
+ const back = Docs.Create.CenteredTextCreator('answer', answer, {});
+ if (useDoc) {
+ useDoc[DocData][frontKey] = front;
+ useDoc[DocData][backKey] = back;
+ return useDoc;
+ }
+ return Docs.Create.FlashcardDocument('flashcard', front, back, { _width: 300, _height: 300 });
};
return keyword && keyword.toLowerCase() !== 'none' ? ComparisonBox.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard();
}
@@ -98,6 +101,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
.map(tuple => ComparisonBox.createFlashcard(tuple, front, back))
).then(docs => {
return Docs.Create.CarouselDocument(docs, {
+ title: 'flashcard deck',
_width: width,
_height: height,
_layout_fitWidth: false,
@@ -116,12 +120,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
private _sideBtnWidth = 35;
private _closeRef = React.createRef<HTMLDivElement>();
private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {};
- private _reactDisposer: IReactionDisposer | undefined;
+ private _reactDisposer: { [key: string]: IReactionDisposer } = {};
@observable private _inputValue = '';
@observable private _outputValue = '';
@observable private _loading = false;
- @observable private _isEmpty = false;
@observable private _childActive = false;
@observable private _animating = '';
@observable private _listening = false;
@@ -135,18 +138,28 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
componentDidMount() {
this._props.setContentViewBox?.(this);
- this._reactDisposer = reaction(
- () => this._props.isSelected(), // when this reaction should update
+ this._reactDisposer.select = reaction(
+ () => this._props.isSelected(),
selected => {
- if (selected && this.isFlashcard) this.activateContent();
+ if (selected && this.revealOp !== flashcardRevealOp.SLIDE) this.activateContent();
!selected && (this._childActive = false);
}, // what it should update to
{ fireImmediately: true }
);
+ this._reactDisposer.hover = reaction(
+ () => this._props.isContentActive(),
+ hover => {
+ if (!hover) {
+ this.revealOp === flashcardRevealOp.FLIP && this.animateFlipping(this.frontKey);
+ this.revealOp === flashcardRevealOp.SLIDE && this.animateSliding(this._props.PanelWidth() - 3);
+ }
+ }, // what it should update to
+ { fireImmediately: true }
+ );
}
componentWillUnmount() {
- this._reactDisposer?.();
+ Object.values(this._reactDisposer).forEach(disposer => disposer?.());
}
protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => {
@@ -169,7 +182,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
return undefined;
}, 'internal drop');
- @computed get isQuizMode() { return DocCast(this.Document.embedContainer)?.practiceMode === practiceMode.QUIZ; } // prettier-ignore
+ @computed get containerDoc() { return this._props.docViewPath().slice(-2)[0]?.Document; } // prettier-ignore
+ @computed get isQuizMode() { return this.containerDoc?.practiceMode === practiceMode.QUIZ; } // prettier-ignore
@computed get isFlashcard() { return BoolCast(this.Document.layout_isFlashcard); } // prettier-ignore
@computed get frontKey() { return this._props.fieldKey + '_front'; } // prettier-ignore
@computed get backKey() { return this._props.fieldKey + '_back'; } // prettier-ignore
@@ -178,10 +192,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
@computed get revealOpKey() { return `_${this._props.fieldKey}_revealOp`; } // prettier-ignore
@computed get clipHeightKey() { return `_${this._props.fieldKey}_clipHeight`; } // prettier-ignore
@computed get clipWidthKey() { return `_${this._props.fieldKey}_clipWidth`; } // prettier-ignore
- @computed get clipWidth() { return NumCast(this.layoutDoc[this.clipWidthKey], 50); } // prettier-ignore
+ @computed get clipWidth() { return NumCast(this.layoutDoc[this.clipWidthKey], this.isFlashcard ? 100: 50); } // prettier-ignore
@computed get clipHeight() { return NumCast(this.layoutDoc[this.clipHeightKey], 200); } // prettier-ignore
- @computed get revealOp() { return StrCast(this.layoutDoc[this.revealOpKey], StrCast(this._props.docViewPath().slice(-2)[0]?.Document.revealOp)); } // prettier-ignore
- set revealOp(value:string) { this.layoutDoc[this.revealOpKey] = value; } // prettier-ignore
+ @computed get revealOp() { return StrCast(this.layoutDoc[this.revealOpKey], StrCast(this.containerDoc?.revealOp, this.isFlashcard ? flashcardRevealOp.FLIP : flashcardRevealOp.SLIDE)) as flashcardRevealOp; } // prettier-ignore
+ @computed get revealOpHover() { return BoolCast(this.layoutDoc[this.revealOpKey+"_hover"], BoolCast(this.containerDoc?.revealOp_hover)); } // prettier-ignore
@computed get loading() { return this._loading; } // prettier-ignore
set loading(value) { runInAction(() => { this._loading = value; })} // prettier-ignore
@@ -193,13 +207,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
onPointerDown={e =>
setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
if (!this.revealOp || this.revealOp === flashcardRevealOp.FLIP) {
- this.flipFlashcard();
+ this.animateFlipping();
}
})
}
style={{
- background: this.revealOp === flashcardRevealOp.HOVER ? 'gray' : this._renderSide === this.backKey ? 'white' : 'black',
- color: this.revealOp === flashcardRevealOp.HOVER ? 'black' : this._renderSide === this.backKey ? 'black' : 'white',
+ background: this.revealOpHover ? 'gray' : this._renderSide === this.backKey ? 'white' : 'black',
+ color: this.revealOpHover ? 'black' : this._renderSide === this.backKey ? 'black' : 'white',
display: 'inline-block',
}}>
<FontAwesomeIcon icon="turn-up" size="xl" />
@@ -223,7 +237,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
@computed get flashcardMenu() {
return SnappingManager.HideDecorations ? null : (
<div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaling})` }}>
- {this.revealOp === flashcardRevealOp.HOVER || !this._props.isSelected() ? null : this.overlayAlternateIcon}
+ {this.revealOpHover || !this._props.isSelected() ? null : this.overlayAlternateIcon}
{!this._props.isSelected() || this._renderSide === this.frontKey ? null : (
<Tooltip title={<div className="dash-tooltip">Ask GPT to create an answer for the question on the front</div>}>
<div className="comparisonBox-button" onPointerDown={() => this.askGPT(GPTCallType.CHATCARD)}>
@@ -296,13 +310,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc);
addDoc = (doc: Doc, which: string) => {
- if (this.dataDoc[which] && !this._isEmpty) return false;
this.dataDoc[which] = doc;
return true;
};
remDoc = (doc: Doc, which: string) => {
if (this.dataDoc[which] === doc) {
- this._isEmpty = true;
this.dataDoc[which] = undefined;
return true;
}
@@ -334,6 +346,26 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
moveDocBack = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.backKey), true);
remDocFront = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.frontKey), true);
remDocBack = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.backKey), true);
+ animateSliding = action((targetWidth: number) => {
+ this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth
+ this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth();
+ setTimeout(action(() => {this._animating = ''; }), this._slideTiming); // prettier-ignore
+ });
+
+ _flipAnim: NodeJS.Timeout | undefined;
+ animateFlipping = action((side?: string) => {
+ if (side !== this._renderSide) {
+ this._renderSide = side ?? (this._renderSide === this.frontKey ? this.backKey : this.frontKey); // switches to new front
+ this._animating = '0'; // reveals old front on the bottom layer by making top layer transparent
+ setTimeout(
+ action(() => {
+ this._animating = `all ${this._slideTiming * 5}ms`; // makes new front fade in
+ clearTimeout(this._flipAnim);
+ this._flipAnim = setTimeout( action(() => { this._animating = ''; }), this._slideTiming * 5 ); // prettier-ignore
+ })
+ );
+ }
+ });
registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => {
if (e.button !== 2) {
@@ -351,13 +383,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
}),
false,
undefined,
- action(() => {
- if (!this._childActive) {
- this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth
- this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth();
- setTimeout( action(() => {this._animating = ''; }), this._slideTiming); // prettier-ignore
- }
- })
+ action(() => !this._childActive && this.animateSliding(targetWidth))
);
}
};
@@ -584,16 +610,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
}
};
- @action
- flipFlashcard = () => {
- this._renderSide = this._renderSide === this.frontKey ? this.backKey : this.frontKey;
- };
-
- @action
- hoverFlip = (side: string) => {
- if (this.revealOp === flashcardRevealOp.HOVER) this._renderSide = side;
- };
-
flashcardContextMenu = () => {
const appearance = ContextMenu.Instance.findByDescription('Appearance...');
const appearanceItems = appearance?.subitems ?? [];
@@ -680,16 +696,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
};
displayBox = (which: string, cover: number) => (
- <div
- className={`${which === this.frontKey ? 'before' : 'after'}Box-cont`}
- key={which}
- style={{ width: this._props.PanelWidth() }}
- onPointerDown={e => {
- this.registerSliding(e, cover);
- this.isFlashcard && this.activateContent();
- }}
- ref={ele => this.createDropTarget(ele, which)}>
- {!this._isEmpty ? this.displayDoc(which) : null}
+ <div className={`${which === this.frontKey ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which)}>
+ {this.displayDoc(which)}
</div>
);
@@ -727,7 +735,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
<button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}>
Evaluate Pronunciation
</button>
- <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.backKey ? this.flipFlashcard : this.handleRenderGPTClick}>
+ <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.backKey ? () => this.animateFlipping(this.frontKey) : this.handleRenderGPTClick}>
{this._renderSide === this.backKey ? 'Redo the Question' : 'Submit'}
</button>
</div>
@@ -736,40 +744,33 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
);
// if flashcard is rendered that has no data, then add some placeholders for question and answer
- addPlaceholdersForEmptyFlashcard = () => {
- if (this.dataDoc.data) {
- if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) ComparisonBox.createFlashcard(StrCast(this.dataDoc.data), this.frontKey, this.backKey, this.Document);
- } else {
- // add text box to each side when comparison box is first created
- if (!this.dataDoc[this.backKey] && !this._isEmpty) {
- this.dataDoc[this.backKey] = FormattedTextBox.centeredTextCreator('answer', 'answer here', undefined, true);
- }
-
- if (!this.dataDoc[this.frontKey] && !this._isEmpty) {
- this.dataDoc[this.frontKey] = FormattedTextBox.centeredTextCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards', undefined, true);
- }
- }
- };
-
- renderAsFlashcard = () => (
+ // addPlaceholdersForEmptyFlashcard = () => {
+ // if (this.dataDoc.data) {
+ // if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) ComparisonBox.createFlashcard(StrCast(this.dataDoc.data), this.frontKey, this.backKey, this.Document);
+ // }
+ // };
+
+ // render a button that flips between front and back
+ renderAsFlip = () => (
<div
- className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
- onContextMenu={this.flashcardContextMenu}
- onMouseEnter={() => this.hoverFlip(this.backKey)}
- onMouseLeave={() => this.hoverFlip(this.frontKey)}>
- {this.displayBox(this._renderSide, this._props.PanelWidth() - 3)}
- {this.loading ? (
- <div className="loading-spinner">
- <ReactLoading type="spin" height={30} width={30} color="blue" />
- </div>
- ) : null}
+ style={{ display: 'flex', pointerEvents: this.revealOpHover && this._props.isContentActive() ? 'unset' : undefined }} //
+ onMouseEnter={() => this.revealOpHover && this.animateFlipping(this.backKey)}
+ onMouseLeave={() => this.revealOpHover && this.animateFlipping(this.frontKey)}>
+ <div style={{ position: 'absolute', width: '100%', height: '100%', transition: this._animating === '0' ? undefined : this._animating, opacity: this._animating === '0' ? 1 : 0 }}>
+ {this.displayBox(this._renderSide === this.backKey ? this.frontKey : this.backKey, 0)}
+ </div>
+ <div style={{ position: 'absolute', width: '100%', height: '100%', transition: this._animating === '0' ? undefined : this._animating, opacity: this._animating === '0' ? 0 : 1 }}>{this.displayBox(this._renderSide, 0)}</div>
{this.flashcardMenu}
</div>
);
- // render a comparison box that compares items side by side
+ // render a slider that reveals front and back as slider is dragged horizonally
renderAsBeforeAfter = () => (
- <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
+ <div
+ className="comparisonBox-slide"
+ style={{ display: 'flex', pointerEvents: this.revealOpHover && this._props.isContentActive() ? 'unset' : undefined }}
+ onMouseEnter={() => this.revealOpHover && this.animateSliding(0)}
+ onMouseLeave={() => this.revealOpHover && this.animateSliding(this._props.PanelWidth() - 3)}>
{this.displayBox(this.backKey, this._props.PanelWidth() - 3)}
<div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
{this.displayBox(this.frontKey, 0)}
@@ -789,11 +790,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
);
render() {
- this.isFlashcard && this.addPlaceholdersForEmptyFlashcard();
- return this.isFlashcard ?
- this.isQuizMode ? this.renderAsQuiz(this.frontText) :
- this.renderAsFlashcard() :
- this.renderAsBeforeAfter(); // prettier-ignore
+ const renderMode = new Map<flashcardRevealOp, () => JSX.Element>([
+ [flashcardRevealOp.FLIP, this.renderAsFlip],
+ [flashcardRevealOp.SLIDE, this.renderAsBeforeAfter]]); // prettier-ignore
+ if (this.isQuizMode) this.renderAsQuiz(this.frontText);
+ return (
+ <div className="comparisonBox" style={{ pointerEvents: this._props.isContentActive() ? 'unset' : undefined }} onContextMenu={this.flashcardContextMenu}>
+ {renderMode.get(this.revealOp)?.() ?? null}
+ {this.loading ? (
+ <div className="loading-spinner">
+ <ReactLoading type="spin" height={30} width={30} color="blue" />
+ </div>
+ ) : null}
+ </div>
+ );
}
}