import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, returnFalse, returnOne, returnZero } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; import { Doc, DocListCast, Field, FieldResult, FieldType, Opt, StrListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { RichTextField } from '../../fields/RichTextField'; import { DocCast, NumCast, StrCast } from '../../fields/Types'; import { DocUtils } from '../documents/DocUtils'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { Docs } from '../documents/Documents'; import { SearchUtil } from '../util/SearchUtil'; import { Transform } from '../util/Transform'; import { ObservableReactComponent } from './ObservableReactComponent'; import './SidebarAnnos.scss'; import { StyleProp } from './StyleProp'; import { CollectionStackingView } from './collections/CollectionStackingView'; import { DocumentView } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; import { FocusViewOptions } from './nodes/FocusViewOptions'; interface ExtraProps { fieldKey: string; Doc: Doc; layoutDoc: Doc; dataDoc: Doc; // usePanelWidth: boolean; showSidebar: boolean; nativeWidth: number; usePanelWidth?: boolean; whenChildContentsActiveChanged: (isActive: boolean) => void; ScreenToLocalTransform: () => Transform; sidebarAddDocument: (doc: Doc | Doc[], suffix: string) => boolean; removeDocument: (doc: Doc | Doc[], suffix: string) => boolean; moveDocument: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean, annotationKey?: string) => boolean; } @observer export class SidebarAnnos extends ObservableReactComponent { constructor(props: FieldViewProps & ExtraProps) { super(props); makeObservable(this); } _stackRef = React.createRef(); @computed get allMetadata() { const keys = new Map>(); DocListCast(this._props.Doc[this.sidebarKey]).forEach(doc => SearchUtil.documentKeys(doc) .filter(key => key[0] && key[0] !== '_' && key[0] === key[0].toUpperCase()) .map(key => keys.set(key, doc[key])) ); return keys; } @computed get allHashtags() { const keys = new Set(); DocListCast(this._props.Doc[this.sidebarKey]).forEach(doc => StrListCast(doc.tags).forEach(tag => keys.add(tag))); return Array.from(keys.keys()) .filter(key => key[0]) .filter(key => !key.startsWith('_') && (key[0] === '#' || key[0] === key[0].toUpperCase())) .sort(); } @computed get allUsers() { const keys = new Set(); DocListCast(this._props.Doc[this.sidebarKey]).forEach(doc => keys.add(StrCast(doc.author))); return Array.from(keys.keys()).sort(); } anchorMenuClick = (anchor: Doc, filterExlusions?: string[]) => { const startup = this.childFilters() .map(filter => filter.split(':')[0]) .join(' '); const target = Docs.Create.TextDocument(startup, { title: '-note-', annotationOn: this._props.Doc, _width: 200, _height: 50, _layout_fitWidth: true, _layout_autoHeight: true, text_fontSize: StrCast(Doc.UserDoc().fontSize), text_fontFamily: StrCast(Doc.UserDoc().fontFamily), }); DocumentView.SetSelectOnLoad(target); DocUtils.MakeLink(anchor, target, { link_relationship: 'inline comment:comment on' }); const taggedContent = this.childFilters() .filter(data => data.split(':')[0]) .filter(data => !filterExlusions?.includes(data.split(':')[0])) .map(data => { const key = '$' + data.split(':')[0]; const val = Field.Copy(this.allMetadata.get(key)); target[key] = val; return { type: 'dashField', attrs: { fieldKey: key, docId: '', hideKey: false, hideValue: false, editable: true }, marks: [{ type: 'pFontSize', attrs: { fontSize: '12px' } }, { type: 'strong' }, { type: 'user_mark', attrs: { userid: ClientUtils.CurrentUserEmail(), modified: 0 } }], }; }); if (!anchor.text) anchor.$text = '-selection-'; const textLines: { type: string; attrs: object; content?: unknown[] }[] = [ { type: 'paragraph', attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, content: [ { type: 'dashField', marks: [ { type: 'linkAnchor', attrs: { allAnchors: [{ href: `/doc/${target[Id]}`, title: 'Anchored Selection', anchorId: `${target[Id]}` }], location: 'add:right', title: 'Anchored Selection', noPreview: true, docref: false, }, }, { type: 'pFontSize', attrs: { fontSize: '8px' } }, { type: 'em' }, ], attrs: { fieldKey: 'text', docId: anchor[Id], hideKey: true, hideValue: false, editable: false }, }, ], }, { type: 'paragraph', attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null } }, ]; const metadatatext = { type: 'paragraph', attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, content: taggedContent, }; if (taggedContent.length) textLines.push(metadatatext); if (textLines.length) { target.$text = new RichTextField( JSON.stringify({ doc: { type: 'doc', content: textLines, }, selection: { type: 'text', anchor: 4, head: 4 }, // set selection to middle paragraph }), '' ); } this.addDocument(target); setTimeout(() => this._stackRef.current?.focusDocument(target, {})); return target; }; makeDocUnfiltered = (doc: Doc) => { if (DocListCast(this._props.Doc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { if (this.childFilters().length) { // if any child filters exist, get rid of them this._props.layoutDoc._childFilters = new List(); return true; } } return false; }; public static getView(sidebar: SidebarAnnos | null, sidebarShown: boolean, toggleSidebar: () => void, doc: Doc, options: FocusViewOptions) { if (!sidebarShown) { options.didMove = true; toggleSidebar(); } options.didMove = sidebar?.makeDocUnfiltered(doc) || options.didMove; if (!doc.hidden) { if (!options.didMove && options.toggleTarget) { options.toggleTarget = false; options.didMove = doc.hidden = true; } } else { options.didMove = !(doc.hidden = false); } return new Promise>(res => DocumentView.addViewRenderedCb(doc, res)); } get sidebarKey() { return this._props.fieldKey + '_sidebar'; } filtersHeight = () => 38; screenToLocalTransform = () => this._props .ScreenToLocalTransform() .translate(Doc.NativeWidth(this._props.dataDoc), 0) .scale(this._props.NativeDimScaling?.() || 1); panelWidth = () => !this._props.showSidebar ? 0 : this._props.usePanelWidth // [DocumentType.RTF, DocumentType.MAP].includes(this._props.layoutDoc.type as any) ? this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) : ((NumCast(this._props.nativeWidth) - Doc.NativeWidth(this._props.dataDoc)) * this._props.PanelWidth()) / NumCast(this._props.nativeWidth); panelHeight = () => this._props.PanelHeight() - this.filtersHeight(); addDocument = (doc: Doc | Doc[]) => this._props.sidebarAddDocument(doc, this.sidebarKey); moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this._props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey); removeDocument = (doc: Doc | Doc[]) => this._props.removeDocument(doc, this.sidebarKey); childFilters = () => StrListCast(this._props.layoutDoc._childFilters); layout_showTitle = () => 'title'; setHeightCallback = (height: number) => this._props.setHeight?.(height + this.filtersHeight()); sortByLinkAnchorY = (a: Doc, b: Doc) => { const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1)?.y; const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1)?.y; return NumCast(ay) - NumCast(by); }; render() { const renderTag = (tag: string) => { const active = this.childFilters().includes(`tags${Doc.FilterSep}${tag}${Doc.FilterSep}check`); return (
Doc.setDocFilter(this._props.Doc, 'tags', tag, 'check', true, undefined, e.shiftKey)}> {tag}
); }; const renderMeta = (tag: string) => { const active = this.childFilters().includes(`${tag}${Doc.FilterSep}${Doc.FilterAny}${Doc.FilterSep}exists`); return (
Doc.setDocFilter(this._props.Doc, tag, Doc.FilterAny, 'exists', true, undefined, e.shiftKey)}> {tag}
); }; const renderUsers = (user: string) => { const active = this.childFilters().includes(`author:${user}:check`); return (
Doc.setDocFilter(this._props.Doc, 'author', user, 'check', true, undefined, e.shiftKey)}> {user}
); }; // TODO: Calculation of the topbar is hardcoded and different for text nodes - it should all be the same and all be part of SidebarAnnos return !this._props.showSidebar ? null : (
e.stopPropagation()}> {this.allUsers.length > 1 ? this.allUsers.map(renderUsers) : null} {this.allHashtags.map(renderTag)} {Array.from(this.allMetadata.keys()).sort().map(renderMeta)}
); } }