aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/PropertiesView.tsx7
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.scss1
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx70
-rw-r--r--src/client/views/collections/CollectionCarouselView.scss86
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx316
-rw-r--r--src/client/views/collections/FlashcardPracticeUI.scss64
-rw-r--r--src/client/views/collections/FlashcardPracticeUI.tsx172
-rw-r--r--src/client/views/nodes/ComparisonBox.scss15
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx92
-rw-r--r--src/client/views/nodes/FieldView.tsx1
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss6
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx16
12 files changed, 475 insertions, 371 deletions
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 371d34173..442dab671 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -139,6 +139,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
Object.values(this._disposers).forEach(disposer => disposer?.());
}
+ @computed get isText() {
+ return this.selectedDoc?.type === DocumentType.RTF;
+ }
@computed get isInk() {
return this.selectedDoc?.type === DocumentType.INK;
}
@@ -1199,8 +1202,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
// prettier-ignore
<div className="transform-editor">
{!this.isStack ? null : this.getNumber('Gap', ' px', 0, 200, NumCast(this.selectedDoc!.gridGap), this.setVal((doc: Doc, val: number) => { doc.gridGap = val; })) }
- {!this.isStack ? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), this.setVal((doc: Doc, val: number) => { doc.xMargin = val; })) }
- {!this.isStack ? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), this.setVal((doc: Doc, val: number) => { doc.yMargin = val; })) }
+ {!this.isStack && !this.isText? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), this.setVal((doc: Doc, val: number) => { doc.xMargin = val; })) }
+ {!this.isStack && !this.isText? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), this.setVal((doc: Doc, val: number) => { doc.yMargin = val; })) }
{!this.isGroup ? null : this.getNumber('Padding', ' px', 0, 500, NumCast(this.selectedDoc!.xPadding), this.setVal((doc: Doc, val: number) => { doc.xPadding = doc.yPadding = val; })) }
{this.isInk ? this.controlPointsButton : null}
{this.getNumber('Width', ' px', 0, Math.max(1000, this.shapeWid), this.shapeWid, this.setVal((doc: Doc, val:number) => {this.shapeWid = val}), 1000, 1)}
diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss
index a556d0fa7..42e112906 100644
--- a/src/client/views/collections/CollectionCarousel3DView.scss
+++ b/src/client/views/collections/CollectionCarousel3DView.scss
@@ -4,6 +4,7 @@
position: relative;
background-color: white;
overflow: hidden;
+ display: flex;
}
.carousel-wrapper {
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index c5da8e037..1583f0e0c 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { computed, makeObservable } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { returnZero } from '../../../ClientUtils';
@@ -9,12 +9,13 @@ import { Id } from '../../../fields/FieldSymbols';
import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
+import { Transform } from '../../util/Transform';
import { StyleProp } from '../StyleProp';
import { DocumentView } from '../nodes/DocumentView';
import { FocusViewOptions } from '../nodes/FocusViewOptions';
import './CollectionCarousel3DView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import { Transform } from '../../util/Transform';
+import { FlashcardPracticeUI } from './FlashcardPracticeUI';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
@@ -24,6 +25,9 @@ export class CollectionCarousel3DView extends CollectionSubView() {
@computed get scrollSpeed() {
return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; // default scroll speed
}
+ _sideBtnWidth = 35;
+ @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+
constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
@@ -43,7 +47,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
};
@computed get carouselItems() {
- return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK);
+ return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK).filter(pair => !this._filterFunc?.(pair.layout));
}
centerScale = Number(CAROUSEL3D_CENTER_SCALE);
@@ -53,22 +57,17 @@ export class CollectionCarousel3DView extends CollectionSubView() {
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
isChildContentActive = () => !!this.isContentActive();
+ contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
childScreenLeftToLocal = () =>
- this._props
- .ScreenToLocalTransform()
- .scale(this._props.NativeDimScaling?.() || 1)
+ this.contentScreenToLocalXf()
.translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
.scale(1 / this.sideScale);
childScreenRightToLocal = () =>
- this._props
- .ScreenToLocalTransform()
- .scale(this._props.NativeDimScaling?.() || 1)
+ this.contentScreenToLocalXf()
.translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
.scale(1 / this.sideScale);
childCenterScreenToLocal = () =>
- this._props
- .ScreenToLocalTransform()
- .scale(this._props.NativeDimScaling?.() || 1)
+ this.contentScreenToLocalXf()
.translate(
-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3
-((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2
@@ -119,7 +118,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
changeSlide = (direction: number) => {
DocumentView.DeselectAll();
- this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % this.carouselItems.length;
+ this.layoutDoc._carousel_index = !this.curDoc() ? 0 : (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % (this.carouselItems.length || 1);
};
onArrowClick = (direction: number) => {
@@ -192,6 +191,35 @@ export class CollectionCarousel3DView extends CollectionSubView() {
return this.panelWidth() * (1 - index);
}
+ /**
+ * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
+ */
+ @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
+
+ /**
+ * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+ */
+ @computed get maxWidgetScale() {
+ const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1));
+ return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
+ }
+ /**
+ * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+ */
+ @computed get uiBtnScaleTransform() { return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore
+ screenXPadding = () => (this.uiBtnScaleTransform * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale;
+
+ docViewProps = () => ({
+ ...this._props, //
+ isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
+ isContentActive: this.isChildContentActive,
+ ScreenToLocalTransform: this.contentScreenToLocalXf,
+ });
+ carouselItemsFunc = () => this.carouselItems.map(pair => pair.layout);
+ @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore
+ answered = (correct: boolean) => (!correct || !this.curDoc()) && this.changeSlide(1);
+ curDoc = () => this.carouselItems[NumCast(this.layoutDoc._carousel_index)]?.layout;
+
render() {
return (
<div
@@ -206,6 +234,22 @@ export class CollectionCarousel3DView extends CollectionSubView() {
</div>
{this.buttons}
<div className="dot-bar">{this.dots}</div>
+ <FlashcardPracticeUI
+ setFilterFunc={this.setFilterFunc}
+ fieldKey={this.fieldKey}
+ sideBtnWidth={this._sideBtnWidth}
+ carouselItems={this.carouselItemsFunc}
+ childDocs={this.childDocs}
+ advance={this.answered}
+ curDoc={this.curDoc}
+ practiceBtnOffset={this._sideBtnWidth * 4}
+ layoutDoc={this.layoutDoc}
+ maxWidgetScale={this.maxWidgetScale}
+ uiBtnScaleTransform={this.uiBtnScaleTransform}
+ ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
+ renderDepth={this._props.renderDepth}
+ docViewProps={this.docViewProps}
+ />
</div>
);
}
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index 97952822e..757072453 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -12,23 +12,10 @@
display: inline-block;
width: 100%;
user-select: none;
+ position: absolute;
+ top: 0;
+ left: 0;
}
- .message {
- justify-content: center;
- align-items: center;
- display: flex;
- height: 60%;
- z-index: -1;
- // margin: 15px;
- }
-}
-
-.collectionCarouselView-addFlashcards {
- justify-content: center;
- align-items: center;
- height: 100%;
- z-index: -1;
- pointer-events: none;
}
.collectionCarouselView-recentlyMissed {
color: red;
@@ -39,10 +26,7 @@
pointer-events: none;
}
.carouselView-back,
-.carouselView-fwd,
-.carouselView-remove,
-.carouselView-check,
-.carouselView-add {
+.carouselView-fwd {
position: absolute;
display: flex;
width: 30;
@@ -66,68 +50,6 @@
left: 0;
transform-origin: top left;
}
-.carouselView-add {
- position: absolute;
- bottom: 0;
- left: 0;
-}
-.carouselView-remove {
- left: 52%;
-}
-.carouselView-check {
- right: 52%;
-}
-.carouselView-menu {
- position: absolute;
- flex-direction: column;
- align-items: center;
- display: flex;
- top: 0px;
- left: 0px;
- width: 30;
- transform-origin: top left;
- border-radius: 5px;
- color: rgba(255, 255, 255, 0.5);
- background: rgba(0, 0, 0, 0.1);
- .carouselView-practiceModes {
- width: 100%;
- display: flex;
- flex-direction: column;
- top: 0;
- position: relative;
- .carouselView-quiz {
- position: relative;
- display: flex;
- height: 20px;
- align-items: center;
- margin: auto;
- &:hover {
- color: white;
- }
- & > svg {
- height: 100%;
- width: 100%;
- }
- }
-
- .carouselView-practice {
- position: relative;
- display: flex;
- flex-direction: column;
- height: 20px;
- align-items: center;
- margin: auto;
- &:hover {
- color: white;
- }
- & > svg {
- height: 100%;
- width: 100%;
- }
- }
- }
-}
-
.carouselView-back:hover,
.carouselView-fwd:hover {
background: lightgray;
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 559dcfe2a..64ddaac79 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -1,52 +1,35 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@mui/material';
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, DocListCast, Opt } from '../../../fields/Doc';
+import { Doc, Opt } from '../../../fields/Doc';
import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
import { StyleProp } from '../StyleProp';
-import { TagItem } from '../TagsView';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import './CollectionCarouselView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { FlashcardPracticeUI } from './FlashcardPracticeUI';
-enum cardMode {
- STAR = 'star',
- ALL = 'all',
-}
-enum practiceMode {
- PRACTICE = 'practice',
- 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 sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore
- get starField() { return "#star"; } // prettier-ignore
-
- _sideBtnWidth = 35;
_fadeTimer: NodeJS.Timeout | undefined;
+ _sideBtnWidth = 35;
+ @observable _filterFunc: ((doc: Doc) => boolean) | undefined = 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;
-
componentWillUnmount() {
this._dropDisposer?.();
}
@@ -58,39 +41,32 @@ export class CollectionCarouselView extends CollectionSubView() {
}
};
- @computed get practiceMode() {
- return this.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this.layoutDoc.practiceMode) : '';
- }
- @computed get practiceMessage() {
- const cardCount = this.carouselItems.length;
- if (this.practiceMode) {
- if (!Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) {
- return 'Finished! Click here to view all flashcards.';
- }
- }
- return '';
- }
-
- @computed get filterMessage() {
- const cardCount = this.carouselItems.length;
- if (!this.practiceMessage) {
- if (Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) {
- return 'No tagged items. Click here to view all flash cards.';
- }
- if (this.practiceMode) {
- if (!cardCount) return 'No flashcards to show! Click here to leave practice mode';
- }
- }
- return '';
- }
- @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 => !this.practiceMode || (BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] !== practiceVal.CORRECT))// show only cards that aren't marked as correct
+ .filter(doc => !this._filterFunc?.(doc))
} // prettier-ignore
/**
+ * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
+ */
+ @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
+
+ /**
+ * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+ */
+ @computed get maxWidgetScale() {
+ const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1));
+ return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
+ }
+ /**
+ * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+ */
+ @computed get uiBtnScaleTransform() { return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore
+ screenXPadding = () => (this.uiBtnScaleTransform * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale;
+
+ /**
* Move forward or backward the specified number of Docs
* @param dir signed number indicating Docs to move forward or backward
*/
@@ -102,8 +78,8 @@ export class CollectionCarouselView extends CollectionSubView() {
/**
* Goes to the next Doc in the stack subject to the currently selected filter option.
*/
- advance = (e: React.MouseEvent) => {
- e.stopPropagation();
+ advance = (e?: React.MouseEvent) => {
+ e?.stopPropagation();
this.move(1);
};
@@ -115,55 +91,23 @@ export class CollectionCarouselView extends CollectionSubView() {
this.move(-1);
};
- /*
- * Toggles whether the 'star' metadata field is set on the current Doc
- */
- toggleStar = (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);
- }
- };
-
- /*
- * 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);
- };
-
- /**
- * Sets the practice mode answer style for flashcards
- * @param mode practiceMode or undefined for no practice
- */
- setPracticeMode = (mode: practiceMode | undefined) => {
- this.layoutDoc.practiceMode = mode;
- this.carouselItems?.map(doc => (doc[this.practiceField] = undefined));
- if (mode === practiceMode.QUIZ) this.carouselItems?.map(doc => (doc[this.sideField] = undefined));
- };
+ curDoc = () => this.carouselItems[this.carouselIndex];
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);
};
+ contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin);
onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
- captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
+ captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX;
contentScreenToLocalXf = () =>
this._props
.ScreenToLocalTransform()
.translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin))
.scale(this._props.NativeDimScaling?.() || 1);
-
- contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
-
isChildContentActive = () =>
this._props.isContentActive?.() === false
? false
@@ -172,10 +116,7 @@ export class CollectionCarouselView extends CollectionSubView() {
: this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
? false
: undefined;
-
renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
- const screenScale = this.ScreenToLocalBoxXf().Scale;
- const fitWidthScale = (NumCast(this.Document.width, 1) / NumCast(this.carouselItems[this.carouselIndex]?._width)) * (this._props.NativeDimScaling?.() || 1);
return (
<DocumentView
{...this._props}
@@ -203,7 +144,7 @@ export class CollectionCarouselView extends CollectionSubView() {
ScreenToLocalTransform={this.contentScreenToLocalXf}
PanelWidth={this.contentPanelWidth}
PanelHeight={this.contentPanelHeight}
- xPadding={(this._sideBtnWidth * Math.min(this.maxWidgetScale, screenScale * screenScale)) / fitWidthScale} // padding shrinks based on screenScale to maintain its size, and then again by screenSize to get smaller
+ screenXPadding={this.screenXPadding}
/>
);
};
@@ -214,7 +155,7 @@ export class CollectionCarouselView extends CollectionSubView() {
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` }}>
+ <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
@@ -235,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 : (
@@ -253,158 +197,70 @@ 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>
)}
</>
);
}
- togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
- toggleFilterMode = () => Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true);
- setColor = (mode: practiceMode | cardMode, which: string) => (which === mode ? 'white' : 'light gray');
-
- @computed get filterDoc() {
- return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter');
- }
- filterHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this.ScreenToLocalBoxXf().Scale);
- filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
-
- /**
- * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings
- */
- @computed get contentScaling() {
- return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1);
- }
-
- /**
- * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
- */
- @computed get maxWidgetScale() {
- const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.Document.width, 1));
- return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
- }
- /**
- * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
- */
- @computed get uiBtnScaleTransform() {
- return `scale(${this.maxWidgetScale * Math.min(1, this.contentScaling)})`;
- }
- @computed get menu() {
- const curDoc = this.carouselItems?.[this.carouselIndex];
- return (
- <div className="carouselView-menu" style={{ height: this.filterHeight(), width: this.filterHeight(), transform: this.uiBtnScaleTransform }}>
- {!this.filterDoc ? null : (
- <DocumentView
- {...this._props}
- Document={this.filterDoc}
- TemplateDataDocument={undefined}
- LayoutTemplate={this._props.childLayoutTemplate}
- LayoutTemplateString={this._props.childLayoutString}
- renderDepth={this._props.renderDepth + 1}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- fitWidth={undefined}
- showTags={false}
- hideFilterStatus={true}
- 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={true}
- childFilters={this.childDocFilters}
- hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
- addDocument={this._props.addDocument}
- ScreenToLocalTransform={this.contentScreenToLocalXf}
- PanelWidth={this.filterWidth}
- PanelHeight={this.filterHeight}
- />
- )}
- <div
- className="carouselView-practiceModes"
- style={{
- transformOrigin: `0px ${-this.filterHeight()}px`,
- transform: `scale(${Math.max(1, 1 / this.ScreenToLocalBoxXf().Scale / this.maxWidgetScale)})`,
- display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none',
- }}>
- <Tooltip title="Practice flashcards using GPT">
- <div key="back" className="carouselView-quiz" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.QUIZ)}>
- <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.practiceMode))} size="1x" />
- </div>
- </Tooltip>
- <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
- <div key="back" className="carouselView-practice" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.PRACTICE)}>
- <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.practiceMode))} size="1x" />
- </div>
- </Tooltip>
- </div>
- </div>
- );
- }
- @computed get buttons() {
- return (
+ @computed get navButtons() {
+ return this.Document._chromeHidden || !this.curDoc() ? null : (
<>
- <div key="back" className="carouselView-back" style={{ transform: this.uiBtnScaleTransform }} onClick={this.goback}>
+ <div key="back" className="carouselView-back" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.goback}>
<FontAwesomeIcon icon="chevron-left" size="2x" />
</div>
- <div key="fwd" className="carouselView-fwd" style={{ transform: this.uiBtnScaleTransform }} onClick={this.advance}>
+ <div key="fwd" className="carouselView-fwd" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.advance}>
<FontAwesomeIcon icon="chevron-right" size="2x" />
</div>
- {this.practiceMode == practiceMode.PRACTICE ? (
- <div style={{ transform: this.uiBtnScaleTransform, bottom: `${this._sideBtnWidth}px`, height: `${this._sideBtnWidth}px`, position: 'absolute', width: `100%` }}>
- <Tooltip title="Incorrect. View again later.">
- <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}>
- <FontAwesomeIcon icon="xmark" color="red" size="1x" />
- </div>
- </Tooltip>
- <Tooltip title="Correct">
- <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)}>
- <FontAwesomeIcon icon="check" color="green" size="1x" />
- </div>
- </Tooltip>
- </div>
- ) : null}
</>
);
}
+ docViewProps = () => ({
+ ...this._props, //
+ isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
+ isContentActive: this.isChildContentActive,
+ ScreenToLocalTransform: this.contentScreenToLocalXf,
+ });
+ carouselItemsFunc = () => this.carouselItems;
+ answered = () => this.advance();
+ @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore
+
render() {
return (
- <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,
- width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`,
- height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`,
- left: NumCast(this.layoutDoc._xMargin),
- top: NumCast(this.layoutDoc._yMargin),
- }}>
- {!this.practiceMessage && !this.filterMessage ? (
- this.content
- ) : (
- <p
- className="message"
- onClick={() => {
- if (this.filterMessage || this.practiceMessage) {
- this.setPracticeMode(undefined);
- Doc.setDocFilter(this.layoutDoc, 'tags', Doc.FilterAny, 'remove');
- }
- }}>
- {this.filterMessage || this.practiceMessage}
- </p>
- )}
- </div>
- {!this.Document._chromeHidden ? this.menu : null}
- {!this.Document._chromeHidden && this.carouselItems?.[this.carouselIndex] ? this.buttons : null}
+ <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,
+ width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`,
+ height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`,
+ left: NumCast(this.layoutDoc._xMargin),
+ top: NumCast(this.layoutDoc._yMargin),
+ }}>
+ {this.content}
+ <FlashcardPracticeUI
+ setFilterFunc={this.setFilterFunc}
+ fieldKey={this.fieldKey}
+ sideBtnWidth={this._sideBtnWidth}
+ carouselItems={this.carouselItemsFunc}
+ childDocs={this.childDocs}
+ advance={this.answered}
+ curDoc={this.curDoc}
+ layoutDoc={this.layoutDoc}
+ maxWidgetScale={this.maxWidgetScale}
+ uiBtnScaleTransform={this.uiBtnScaleTransform}
+ ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
+ renderDepth={this._props.renderDepth}
+ docViewProps={this.docViewProps}
+ />
+ {this.navButtons}
</div>
);
}
diff --git a/src/client/views/collections/FlashcardPracticeUI.scss b/src/client/views/collections/FlashcardPracticeUI.scss
new file mode 100644
index 000000000..53c26ad34
--- /dev/null
+++ b/src/client/views/collections/FlashcardPracticeUI.scss
@@ -0,0 +1,64 @@
+.FlashcardPracticeUI-remove,
+.FlashcardPracticeUI-check {
+ position: absolute;
+ display: flex;
+ width: 30;
+ height: 30;
+ align-items: center;
+ border-radius: 5px;
+ justify-content: center;
+ color: rgba(255, 255, 255, 0.5);
+ background: rgba(0, 0, 0, 0.1);
+ &:hover {
+ color: white;
+ }
+}
+.FlashcardPracticeUI-remove {
+ left: 52%;
+}
+.FlashcardPracticeUI-check {
+ right: 52%;
+}
+.FlashcardPracticeUI-menu {
+ position: absolute;
+ flex-direction: column;
+ align-items: center;
+ display: flex;
+ top: 0px;
+ left: 0px;
+ width: 30;
+ transform-origin: top left;
+ border-radius: 5px;
+ color: rgba(255, 255, 255, 0.5);
+ background: rgba(0, 0, 0, 0.1);
+ .FlashcardPracticeUI-practiceModes {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ top: 0;
+ position: relative;
+ .FlashcardPracticeUI-quiz,
+ .FlashcardPracticeUI-practice {
+ position: relative;
+ display: flex;
+ height: 20px;
+ align-items: center;
+ margin: auto;
+ padding: 3px;
+ &:hover {
+ color: white;
+ }
+ & > svg {
+ height: 100%;
+ width: 100%;
+ }
+ }
+ }
+}
+.FlashcardPracticeUI-message {
+ z-index: 100;
+ position: relative;
+ margin: auto;
+ align-content: center;
+ width: max-content;
+}
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
new file mode 100644
index 000000000..032a405bf
--- /dev/null
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -0,0 +1,172 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
+import { computed, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { returnZero } from '../../../ClientUtils';
+import { Doc, DocListCast } from '../../../fields/Doc';
+import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { Transform } from '../../util/Transform';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import './FlashcardPracticeUI.scss';
+
+enum practiceMode {
+ PRACTICE = 'practice',
+ QUIZ = 'quiz',
+}
+enum practiceVal {
+ MISSED = 'missed',
+ CORRECT = 'correct',
+}
+
+interface PracticeUIProps {
+ fieldKey: string;
+ layoutDoc: Doc;
+ carouselItems: () => Doc[];
+ childDocs: Doc[];
+ curDoc: () => Doc;
+ advance: (correct: boolean) => void;
+ renderDepth: number;
+ sideBtnWidth: number;
+ uiBtnScaleTransform: number;
+ ScreenToLocalBoxXf: () => Transform;
+ maxWidgetScale: number;
+ docViewProps: () => DocumentViewProps;
+ setFilterFunc: (func?: (doc: Doc) => boolean) => void;
+ practiceBtnOffset?: number;
+}
+@observer
+export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProps> {
+ constructor(props: PracticeUIProps) {
+ super(props);
+ makeObservable(this);
+ this._props.setFilterFunc(this.tryFilterOut);
+ }
+
+ componentWillUnmount(): void {
+ this._props.setFilterFunc(undefined);
+ }
+
+ get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore
+
+ @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
+ @computed get practiceMode() { return this._props.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+
+ btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale);
+ btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
+
+ /**
+ * Sets the practice mode answer style for flashcards
+ * @param mode practiceMode or undefined for no practice
+ */
+ setPracticeMode = (mode: practiceMode | undefined) => {
+ this._props.layoutDoc.practiceMode = mode;
+ this._props.carouselItems().map(doc => (doc[this.practiceField] = undefined));
+ };
+
+ @computed get emptyMessage() {
+ const cardCount = this._props.carouselItems().length;
+ const practiceMessage = this.practiceMode && !Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !this._props.carouselItems().length ? 'Finished! Click here to view all flashcards.' : '';
+ const filterMessage = practiceMessage
+ ? ''
+ : Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !cardCount
+ ? 'No tagged items. Click here to view all flash cards.'
+ : this.practiceMode && !cardCount
+ ? 'No flashcards to show! Click here to leave practice mode'
+ : '';
+ return !practiceMessage && !filterMessage ? null : (
+ <p
+ className="FlashcardPracticeUI-message"
+ onClick={() => {
+ if (filterMessage || practiceMessage) {
+ this.setPracticeMode(undefined);
+ Doc.setDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny, 'remove');
+ }
+ }}>
+ {filterMessage || practiceMessage}
+ </p>
+ );
+ }
+
+ @computed get practiceButtons() {
+ /*
+ * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode.
+ */
+ const setPracticeVal = (e: React.MouseEvent, val: string) => {
+ e.stopPropagation();
+ const curDoc = this._props.curDoc();
+ curDoc && (curDoc[this.practiceField] = val);
+ this._props.advance?.(val === practiceVal.CORRECT);
+ };
+
+ return this.practiceMode == practiceMode.PRACTICE ? (
+ <div style={{ transform: `scale(${this._props.uiBtnScaleTransform})`, bottom: `${this._props.practiceBtnOffset ?? this._props.sideBtnWidth}px`, height: `${this._props.sideBtnWidth}px`, position: 'absolute', width: `100%` }}>
+ <Tooltip title="Incorrect. View again later.">
+ <div key="remove" className="FlashcardPracticeUI-remove" onClick={e => setPracticeVal(e, practiceVal.MISSED)}>
+ <FontAwesomeIcon icon="xmark" color="red" size="1x" />
+ </div>
+ </Tooltip>
+ <Tooltip title="Correct">
+ <div key="check" className="FlashcardPracticeUI-check" onClick={e => setPracticeVal(e, practiceVal.CORRECT)}>
+ <FontAwesomeIcon icon="check" color="green" size="1x" />
+ </div>
+ </Tooltip>
+ </div>
+ ) : null;
+ }
+ @computed get practiceModesMenu() {
+ const setColor = (mode: practiceMode) => (StrCast(this.practiceMode) === mode ? 'white' : 'light gray');
+ const togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
+
+ return !this._props.curDoc()?._layout_isFlashcard ? null : (
+ <div
+ className="FlashcardPracticeUI-practiceModes"
+ style={{
+ transformOrigin: `0px ${-this.btnHeight()}px`,
+ transform: `scale(${Math.max(1, 1 / this._props.ScreenToLocalBoxXf().Scale / this._props.maxWidgetScale)})`,
+ }}>
+ <Tooltip title="Practice flashcards using GPT">
+ <div key="back" className="FlashcardPracticeUI-quiz" style={{ width: this.btnWidth(), height: this.btnHeight() }} onClick={() => togglePracticeMode(practiceMode.QUIZ)}>
+ <FontAwesomeIcon icon="file-pen" color={setColor(practiceMode.QUIZ)} size="sm" />
+ </div>
+ </Tooltip>
+ <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
+ <div key="back" className="FlashcardPracticeUI-practice" style={{ width: this.btnWidth(), height: this.btnHeight() }} onClick={() => togglePracticeMode(practiceMode.PRACTICE)}>
+ <FontAwesomeIcon icon="check" color={setColor(practiceMode.PRACTICE)} size="sm" />
+ </div>
+ </Tooltip>
+ </div>
+ );
+ }
+ tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
+ render() {
+ return (
+ <>
+ {this.emptyMessage}
+ {this.practiceButtons}
+ <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaleTransform})` }}>
+ {!this.filterDoc || this._props.layoutDoc._chromeHidden ? null : (
+ <DocumentView
+ {...this._props.docViewProps()}
+ Document={this.filterDoc}
+ TemplateDataDocument={undefined}
+ PanelWidth={this.btnWidth}
+ PanelHeight={this.btnHeight}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ hideDecorations={BoolCast(this._props.layoutDoc.layout_hideDecorations)}
+ hideCaptions={true}
+ hideFilterStatus={true}
+ renderDepth={this._props.renderDepth + 1}
+ fitWidth={undefined}
+ showTags={false}
+ setContentViewBox={undefined}
+ />
+ )}
+ {this.practiceModesMenu}
+ </div>
+ </>
+ );
+ }
+}
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index b7307f3a3..8156c50f6 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -296,3 +296,18 @@
}
}
}
+.comparisonBox-bottomMenu {
+ transform-origin: bottom right;
+ width: max-content;
+ justify-content: space-between;
+ height: max-content;
+ position: absolute;
+ bottom: 0;
+ right: 2;
+ flex-direction: row-reverse;
+ display: flex;
+ cursor: pointer;
+ .comparisonBox-button {
+ padding-right: 8px;
+ }
+}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index ef66c2b11..81e223028 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -68,7 +68,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
return (
<Tooltip title={<div className="dash-tooltip">flip</div>}>
<div
- className="formattedTextBox-alternateButton"
+ className="comparisonBox-alternateButton ccomparisonBox-button"
onPointerDown={e =>
setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
if (!this.revealOp || this.revealOp === 'flip') {
@@ -81,42 +81,74 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
color: this.revealOp === 'hover' ? 'black' : this._frontSide ? 'black' : 'white',
display: 'inline-block',
}}>
- <div key="alternate" className="formattedTextBox-flip">
- <FontAwesomeIcon icon="turn-up" size="1x" />
- </div>
+ <FontAwesomeIcon icon="turn-up" size="xl" />
</div>
</Tooltip>
);
}
+ _sideBtnWidth = 30;
+ /**
+ * How much the content of the view is being scaled based on its nesting and its fit-to-width settings
+ */
+ @computed get contentScaling() {
+ return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1);
+ }
+ /**
+ * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size.
+ */
+ @computed get maxWidgetScale() {
+ const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height)));
+ return Math.max(maxWidgetSize / this._sideBtnWidth, 1);
+ }
+ /**
+ * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content
+ */
+ @computed get uiBtnScaleTransform() {
+ return this.maxWidgetScale * Math.min(1, this.contentScaling);
+ }
+
@computed get flashcardMenu() {
return (
- <div>
- <Tooltip
- title={this._frontSide ? <div className="dash-tooltip">Flip to front side to use GPT</div> : <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div>}>
- <div style={{ position: 'absolute', bottom: '3px', right: '50px', cursor: 'pointer' }} onPointerDown={() => (!this._frontSide ? this.findImageTags() : null)}>
- <FontAwesomeIcon icon="lightbulb" size="xl" />
- </div>
- </Tooltip>
- {DocCast(this.Document.embedContainer)?.type_collection === CollectionViewType.Carousel ? null : (
- <div>
- <Tooltip title={<div>Create a flashcard pile</div>}>
- <div style={{ position: 'absolute', bottom: '3px', right: '74px', cursor: 'pointer' }} onPointerDown={() => this.createFlashcardPile([this.Document], false)}>
- <FontAwesomeIcon icon="folder-plus" size="xl" />
- </div>
- </Tooltip>
- <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
- <div style={{ position: 'absolute', bottom: '3px', right: '104px', cursor: 'pointer' }} onClick={() => this.gptFlashcardPile()}>
- <FontAwesomeIcon icon="layer-group" size="xl" />
+ <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaleTransform})` }}>
+ {this.overlayAlternateIcon}
+ {!this._props.isContentActive() ? null : (
+ <>
+ {' '}
+ {!this._frontSide ? null : (
+ <Tooltip
+ title={
+ <div className="dash-tooltip">{
+ !this._frontSide ? "Flip to front side to use GPT":
+ "Ask GPT to create an answer on the back side of the flashcard based on your question on the front"}
+ </div> // prettier-ignore
+ }>
+ <div className="comparisonBox-button" onPointerDown={() => (this._frontSide ? this.findImageTags() : null)}>
+ <FontAwesomeIcon icon="lightbulb" size="xl" />
+ </div>
+ </Tooltip>
+ )}
+ {DocCast(this.Document.embedContainer)?.type_collection === CollectionViewType.Carousel ? null : (
+ <>
+ <Tooltip title={<div>Create a flashcard pile</div>}>
+ <div className="comparisonBox-button" onPointerDown={() => this.createFlashcardPile([this.Document], false)}>
+ <FontAwesomeIcon icon="folder-plus" size="xl" />
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
+ <div className="comparisonBox-button" onClick={this.gptFlashcardPile}>
+ <FontAwesomeIcon icon="layer-group" size="xl" />
+ </div>
+ </Tooltip>
+ </>
+ )}
+ <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
+ <div className="comparisonBox-button" onClick={this.handleHover}>
+ <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" />
</div>
</Tooltip>
- </div>
+ </>
)}
- <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
- <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={() => this.handleHover()}>
- <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" />
- </div>
- </Tooltip>
</div>
);
}
@@ -478,7 +510,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
this._loading = false;
return;
}
- this.flipFlashcard();
}
try {
console.log(queryText);
@@ -655,6 +686,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
childActiveFunc = () => this._childActive;
+ contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
render() {
const clearButton = (which: string) => (
<Tooltip title={<div className="dash-tooltip">remove</div>}>
@@ -687,6 +719,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
NativeWidth={returnZero}
NativeHeight={returnZero}
+ ScreenToLocalTransform={this.contentScreenToLocalXf}
isContentActive={this.childActiveFunc}
isDocumentActive={returnFalse}
dontSelect={returnTrue}
@@ -808,8 +841,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
<ReactLoading type="spin" height={30} width={30} color={'blue'} />
</div>
) : null}
- {this._props.isContentActive() ? this.flashcardMenu : null}
- {this.overlayAlternateIcon}
+ {this.flashcardMenu}
</div>
);
}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 170966471..c81631baa 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -51,6 +51,7 @@ export interface FieldViewSharedProps {
LayoutTemplate?: () => Opt<Doc>;
renderDepth: number;
scriptContext?: unknown; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
+ screenXPadding?: () => number; // padding in screen space coordinates (used by text box to reflow around UI buttons in carouselView)
xPadding?: number;
yPadding?: number;
dontRegisterView?: boolean;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index d6f13d9ee..f1ae1151f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -101,12 +101,6 @@ audiotag:hover {
height: 22;
cursor: default;
}
-.formattedTextBox-flip {
- align-items: center;
- position: absolute;
- right: 2px;
- bottom: 4px;
-}
.formattedTextBox-outer {
position: relative;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index c89737e1e..93153b453 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -763,10 +763,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
);
};
sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
- const localDelta = this._props
- .ScreenToLocalTransform()
- .scale(this._props.NativeDimScaling?.() || 1)
- .transformDirection(delta[0], delta[1]);
+ const localDelta = this.DocumentView?.().screenToViewTransform().transformDirection(delta[0], delta[1]) ?? delta;
const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100;
const width = NumCast(this.layoutDoc._width) + localDelta[0];
this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%';
@@ -1264,7 +1261,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this._cachedLinks = Doc.Links(this.Document);
this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation);
this._disposers.layout_autoHeight = reaction(
- () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss] }),
+ () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss], xMargin: this.Document.xMargin, yMargin: this.Document.yMargin }),
autoHeight => setTimeout(() => autoHeight && this.tryUpdateScrollHeight())
);
this._disposers.highlights = reaction(
@@ -2088,8 +2085,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const scale = this._props.NativeDimScaling?.() || 1;
const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : '';
setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
- const paddingX = Math.max(this._props.xPadding ?? 0, NumCast(this.layoutDoc._xMargin));
- const paddingY = Math.max(this._props.yPadding ?? 0, NumCast(this.layoutDoc._yMargin));
+
+ const scrSize = (which: number, view = this._props.docViewPath().slice(-which)[0]) =>
+ [view._props.PanelWidth() / view.screenToLocalScale(), view._props.PanelHeight() / view.screenToLocalScale()]; // prettier-ignore
+ const scrMargin = [Math.max(0, (scrSize(2)[0] - scrSize(1)[0]) / 2), Math.max(0, (scrSize(2)[1] - scrSize(1)[1]) / 2)];
+ const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), this._props.xPadding ?? 0, 0, ((this._props.screenXPadding?.() ?? 0) - scrMargin[0]) * this.ScreenToLocalBoxXf().Scale);
+ const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, ((this._props.yPadding ?? 0) - scrMargin[1]) * this.ScreenToLocalBoxXf().Scale);
const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' >
return styleFromLayout?.height === '0px' ? null : (
<div
@@ -2176,7 +2177,6 @@ Docs.Prototypes.TemplateMap.set(DocumentType.RTF, {
_layout_nativeDimEditable: true,
_layout_reflowVertical: true,
_layout_reflowHorizontal: true,
- _layout_noSidebar: true,
defaultDoubleClick: 'ignore',
systemIcon: 'BsFileEarmarkTextFill',
},