diff options
Diffstat (limited to 'src/client/views/collections/CollectionSubView.tsx')
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 132 |
1 files changed, 119 insertions, 13 deletions
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 581201a20..655894e40 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,7 +1,7 @@ import { action, computed, makeObservable, observable } from 'mobx'; import * as React from 'react'; import * as rp from 'request-promise'; -import { ClientUtils, returnFalse } from '../../../ClientUtils'; +import { ClientUtils, DashColor, returnFalse } from '../../../ClientUtils'; import CursorField from '../../../fields/CursorField'; import { Doc, DocListCast, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc'; import { AclPrivate, DocData } from '../../../fields/DocSymbols'; @@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; @@ -25,7 +25,21 @@ import { SnappingManager } from '../../util/SnappingManager'; import { UndoManager } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { FieldViewProps } from '../nodes/FieldView'; -import { DocumentView } from '../nodes/DocumentView'; +import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import { FlashcardPracticeUI } from './FlashcardPracticeUI'; +import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere'; +import { Upload } from '../../../server/SharedMediaTypes'; + +export enum docSortings { + Time = 'time', + Type = 'type', + Color = 'color', + Chat = 'chat', + Tag = 'tag', + None = '', +} + +export const ChatSortField = 'chat_sortIndex'; export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> { isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc) @@ -111,6 +125,7 @@ export function CollectionSubView<X>() { return this.dataDoc[this._props.fieldKey]; // this used to be 'layoutDoc', but then template fields will get ignored since the template is not a proto of the layout. hopefully nothing depending on the previous code. } + hasChildDocs = () => this.childLayoutPairs.map(pair => pair.layout); @computed get childLayoutPairs(): { layout: Doc; data: Doc }[] { const { Document, TemplateDataDocument } = this._props; const validPairs = this.childDocs @@ -119,7 +134,8 @@ export function CollectionSubView<X>() { pair => // filter out any documents that have a proto that we don't have permissions to !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate)) - ); + ) + .filter(pair => !this._filterFunc?.(pair.layout!)); return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } /** @@ -128,6 +144,17 @@ export function CollectionSubView<X>() { @computed get childDocList() { return Cast(this.dataField, listSpec(Doc)); } + + addLinkedDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => { + const doc = toList(docsIn).lastElement(); + const where = location.split(':')[0]; + if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) { + if (doc.hidden) doc.hidden = false; + if (!location.includes(OpenWhereMod.always)) return true; + } + return this._props.addDocTab(docsIn, location); + }; + collectionFilters = () => this._focusFilters ?? StrListCast(this.Document._childFilters); collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.Document._childFiltersByRanges, listSpec('string'), []); // child filters apply to the descendants of the documents in this collection @@ -136,6 +163,8 @@ export function CollectionSubView<X>() { unrecursiveDocFilters = () => [...(this._props.childFilters?.().filter(f => !ClientUtils.IsRecursiveFilter(f)) || [])]; childDocRangeFilters = () => [...(this._props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()]; searchFilterDocs = () => this._props.searchFilterDocs?.() ?? DocListCast(this.Document._searchFilterDocs); + + @observable docDraggedIndex = -1; @computed.struct get childDocs() { TraceMobx(); let rawdocs: (Doc | Promise<Doc>)[] = []; @@ -152,8 +181,10 @@ export function CollectionSubView<X>() { const templateRoot = this._props.TemplateDataDocument; rawdocs = templateRoot && !this._props.isAnnotationOverlay ? [Doc.GetProto(templateRoot)] : []; } - - const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc); + const childDocs = this.childSortedDocs( + rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc), + this.docDraggedIndex + ); const childDocFilters = this.childDocFilters(); const childFiltersByRanges = this.childDocRangeFilters(); @@ -200,6 +231,33 @@ export function CollectionSubView<X>() { return docsforFilter; } + childSortedDocs = (docsIn: Doc[], dragIndex: number) => { + const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings; + const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_reverse']); + const docs = docsIn.slice(); + sortType && docs.sort((docA, docB) => { + const [typeA, typeB] = (() => { + switch (sortType) { + default: + case docSortings.Type: return [StrCast(docA.type), StrCast(docB.type)]; + case docSortings.Chat: return [NumCast(docA[ChatSortField], 9999), NumCast(docB[ChatSortField], 9999)]; + case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()]; + case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()]; + case docSortings.Tag: return [StrListCast(docA.tags).join(""), StrListCast(docB.tags).join("")]; + } + })(); + return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? -1 : 1); + }); //prettier-ignore + if (dragIndex !== -1) { + const draggedDoc = DragManager.docsBeingDragged[0]; + const originalIndex = docs.findIndex(doc => doc === draggedDoc); + + originalIndex !== -1 && docs.splice(originalIndex, 1); + draggedDoc && docs.splice(dragIndex, 0, draggedDoc); + } + return docs; + }; + @action protected async setCursorPosition(position: [number, number]) { let ind; @@ -350,7 +408,7 @@ export function CollectionSubView<X>() { const imgSrc = img.split('src="')[1].split('"')[0]; const imgOpts = { ...options, _width: 300 }; if (imgSrc.startsWith('data:image') && imgSrc.includes('base64')) { - const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: [imgSrc] })).lastElement(); + const result = ((await Networking.PostToServer('/uploadRemoteImage', { sources: [imgSrc] })) as Upload.ImageInformation[]).lastElement(); const newImgSrc = result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 // ? ClientUtils.prepend(result.accessPaths.agnostic.client) @@ -483,12 +541,17 @@ export function CollectionSubView<X>() { DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { generatedDocuments.push( - ...files.map(file => { - const loading = Docs.Create.LoadingDocument(file, options); - Doc.addCurrentlyLoading(loading); - DocUtils.uploadFileToDoc(file, {}, loading); - return loading; - }) + ...(await Promise.all( + files.map(async file => { + if (file.name.endsWith('svg')) { + return (await DocUtils.openSVGfile(file, options)) as Doc; + } + const loading = Docs.Create.LoadingDocument(file, options); + Doc.addCurrentlyLoading(loading); + DocUtils.uploadFileToDoc(file, {}, loading); + return loading; + }) + )) ); } if (generatedDocuments.length) { @@ -515,6 +578,49 @@ export function CollectionSubView<X>() { alert('Document upload failed - possibly an unsupported file type.'); } }; + + protected _sideBtnWidth = 35; + protected _sideBtnMaxPanelPct = 0.15; + @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined; + /** + * How much the content of the collection is being scaled based on its nesting and its fit-to-width settings + */ + @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore + /** + * The maximum size a UI widget can be in collection coordinates based on not wanting the widget to visually obscure too much of the collection + * This takes the desired screen space size and converts into collection coordinates. It then returns the smaller of the converted + * size or a fraction of the collection view. + */ + @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width)? 1: this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1)); } // prettier-ignore + /** + * This computes a scale factor for UI elements so that they shrink and grow as the collection does in screen space. + * Note, the scale factor does not allow for elements to grow larger than their native screen space size. + */ + @computed get uiBtnScaling() { return this.maxWidgetSize / this._sideBtnWidth; } // prettier-ignore + + screenXPadding = () => (this.uiBtnScaling * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale; + filteredChildDocs = () => this.childLayoutPairs.map(pair => pair.layout); + childDocsFunc = () => this.childDocs; + @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore + + public flashCardUI = (curDoc: () => Doc | undefined, docViewProps: () => DocumentViewProps, answered?: (correct: boolean) => void) => { + return ( + <FlashcardPracticeUI + setFilterFunc={this.setFilterFunc} + fieldKey={this.fieldKey} + sideBtnWidth={this._sideBtnWidth} + allChildDocs={this.childDocsFunc} + filteredChildDocs={this.filteredChildDocs} + advance={answered} + curDoc={curDoc} + layoutDoc={this.layoutDoc} + uiBtnScaling={this.uiBtnScaling} + ScreenToLocalBoxXf={this.ScreenToLocalBoxXf} + renderDepth={this._props.renderDepth} + docViewProps={docViewProps} + /> + ); + }; } return CollectionSubViewInternal; |