diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/KeywordBox.tsx | 390 | ||||
-rw-r--r-- | src/client/views/StyleProvider.tsx | 8 |
2 files changed, 180 insertions, 218 deletions
diff --git a/src/client/views/KeywordBox.tsx b/src/client/views/KeywordBox.tsx index 20cb63d66..703299ae6 100644 --- a/src/client/views/KeywordBox.tsx +++ b/src/client/views/KeywordBox.tsx @@ -3,17 +3,17 @@ import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; import { returnFalse, setupMoveUpEvents } from '../../ClientUtils'; -import { Doc, DocListCast } from '../../fields/Doc'; +import { Utils, emptyFunction } from '../../Utils'; +import { Doc, DocListCast, StrListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { NumCast, StrCast } from '../../fields/Types'; -import { emptyFunction, Utils } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; +import { SelectionManager } from '../util/SelectionManager'; import { SnappingManager } from '../util/SnappingManager'; -import { DocumentView } from './nodes/DocumentView'; -import { ObservableReactComponent } from './ObservableReactComponent'; import { undoable } from '../util/UndoManager'; +import { ObservableReactComponent } from './ObservableReactComponent'; interface KeywordItemProps { doc: Doc; @@ -28,69 +28,149 @@ interface KeywordItemProps { */ @observer export class KeywordItem extends ObservableReactComponent<KeywordItemProps> { - constructor(props: any) { - super(props); - makeObservable(this); - this.ref = React.createRef(); + /** + * return list of all Docs that collect Docs with specified keywords + */ + public static get AllKeywordCollections() { + return DocListCast(Doc.ActiveDashboard?.myKeywordCollections); } + /** + * Find Doc that collects all Docs with given keyword + * @param keyword keyword string + * @returns keyword collection Doc or undefined + */ + public static findKeywordCollectionDoc = (keyword: String) => KeywordItem.AllKeywordCollections.find(doc => doc.title === keyword); - private ref: React.RefObject<HTMLDivElement>; + /** + * Creates a Doc that collects Docs with the specified keyword + * @param keyword keyword string + * @returns collection Doc + */ + public static createKeywordCollectionDoc = (keyword: string) => { + const newKeywordCol = new Doc(); + newKeywordCol.title = keyword; + newKeywordCol.collections = new List<Doc>(); + newKeywordCol[DocData].docs = new List<Doc>(); + // If the active Dashboard does not have a keyword collection, create it. + if (Doc.ActiveDashboard) { + if (!Doc.ActiveDashboard.myKeywordCollections) Doc.ActiveDashboard.myKeywordCollections = new List<Doc>(); + Doc.AddDocToList(Doc.ActiveDashboard, 'myKeywordCollections', newKeywordCol); + } + return newKeywordCol; + }; /** - * Gets the documents that a keyword is associated with. + * Gets all Docs that have the specified keyword + * @param keyword keyword string * @returns An array of documents that contain the keyword. */ - getKeywordCollectionDocs = () => { - for (const doc of DocListCast(Doc.ActiveDashboard?.myKeywordCollections)) { - if (doc.title === this._props.keyword) { - return doc[DocData].docs; + public static allDocsWithKeyword = (keyword: string) => DocListCast(KeywordItem.findKeywordCollectionDoc(keyword)?.[DocData].docs); + + /** + * Adds a keyword to the metadata of this document + * @param keyword keyword string + */ + public static addLabelToDoc = (doc: Doc, keyword: string) => { + // If the keyword collection is not in active Dashboard, add it as a new doc, with the keyword as its title. + const keywordCollection = KeywordItem.findKeywordCollectionDoc(keyword) ?? KeywordItem.createKeywordCollectionDoc(keyword); + + // If the document is of type COLLECTION, make it a smart collection, otherwise, add the keyword to the document. + if (doc.type === DocumentType.COL) { + Doc.AddDocToList(keywordCollection[DocData], 'collections', doc); + + // Iterate through the keyword Doc collections and add a copy of the document to each collection + for (const cdoc of DocListCast(keywordCollection[DocData].docs)) { + if (!DocListCast(doc[DocData].data).find(d => Doc.AreProtosEqual(d, cdoc))) { + const newEmbedding = Doc.MakeEmbedding(cdoc); + Doc.AddDocToList(doc[DocData], 'data', newEmbedding); + Doc.SetContainer(newEmbedding, doc); + } + } + } else { + // Add this document to the keyword's collection of associated documents. + Doc.AddDocToList(keywordCollection[DocData], 'docs', doc); + + // Iterate through the keyword document's collections and add a copy of the document to each collection + for (const collection of DocListCast(keywordCollection.collections)) { + if (!DocListCast(collection[DocData].data).find(d => Doc.AreProtosEqual(d, doc))) { + const newEmbedding = Doc.MakeEmbedding(doc); + Doc.AddDocToList(collection[DocData], 'data', newEmbedding); + Doc.SetContainer(newEmbedding, collection); + } } } - return null; + + if (!doc[DocData].data_labels) doc[DocData].data_labels = new List<string>(); + (doc[DocData].data_labels as List<string>).push(keyword); + doc[DocData][keyword] = true; }; + public static RemoveLabel = (doc: Doc, keyword: string, keywordDoc: Doc) => { + if (doc[DocData].data_labels) { + if (doc.type === DocumentType.COL) { + Doc.RemoveDocFromList(keywordDoc[DocData], 'collections', doc); + + for (const cur_doc of KeywordItem.allDocsWithKeyword(keyword)) { + doc[DocData].data = new List<Doc>(DocListCast(doc[DocData].data).filter(d => !Doc.AreProtosEqual(cur_doc, d))); + } + } else { + Doc.RemoveDocFromList(keywordDoc[DocData], 'docs', doc); + + for (const collection of DocListCast(keywordDoc.collections)) { + collection[DocData].data = new List<Doc>(DocListCast(collection[DocData].data).filter(d => !Doc.AreProtosEqual(doc, d))); + } + } + } + doc[DocData].data_labels = new List<string>((doc[DocData].data_labels as List<string>).filter(label => label !== keyword)); + doc[DocData][keyword] = undefined; + }; + + private _ref: React.RefObject<HTMLDivElement>; + + constructor(props: any) { + super(props); + makeObservable(this); + this._ref = React.createRef(); + } + /** * Creates a smart collection. * @returns */ createCollection = () => { // Get the documents that contain the keyword. - const selected = DocListCast(this.getKeywordCollectionDocs()!); - const newEmbeddings = selected.map(doc => Doc.MakeEmbedding(doc)); + const newEmbeddings = KeywordItem.allDocsWithKeyword(this._props.keyword).map(doc => Doc.MakeEmbedding(doc)); // Create a new collection and set up configurations. const newCollection = ((doc: Doc) => { const docData = doc[DocData]; docData.data = new List<Doc>(newEmbeddings); docData.title = this._props.keyword; + docData.data_labels = new List<string>([this._props.keyword]); + docData[`${this._props.keyword}`] = true; + docData.showLabels = true; doc._freeform_panX = doc._freeform_panY = 0; + doc._width = 900; + doc._height = 900; + doc.layout_fitWidth = true; return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); - newEmbeddings.forEach(embed => (embed.embedContainer = newCollection)); - newCollection._width = 900; - newCollection._height = 900; - newCollection.layout_fitWidth = true; + newEmbeddings.forEach(embed => Doc.SetContainer(embed, newCollection)); // Add the collection to the keyword document's list of associated smart collections. - this._props.keywordDoc.collections = new List<Doc>([...DocListCast(this._props.keywordDoc.collections), newCollection]); - newCollection[DocData].data_labels = new List<string>([this._props.keyword]); - newCollection[DocData][`${this._props.keyword}`] = true; - newCollection[DocData].showLabels = true; + Doc.AddDocToList(this._props.keywordDoc, 'collections', newCollection); return newCollection; }; @action handleDragStart = (e: React.PointerEvent) => { if (this._props.isEditing) { - const clone = this.ref.current?.cloneNode(true) as HTMLElement; - if (!clone) return; - setupMoveUpEvents( this, e, () => { const dragData = new DragManager.DocumentDragData([this.createCollection()]); - DragManager.StartDocumentDrag([this.ref.current!], dragData, e.clientX, e.clientY, {}); + DragManager.StartDocumentDrag([this._ref.current!], dragData, e.clientX, e.clientY, {}); return true; }, returnFalse, @@ -100,51 +180,34 @@ export class KeywordItem extends ObservableReactComponent<KeywordItemProps> { } }; - @action - removeLabel = () => { - if (this._props.doc[DocData].data_labels) { - if (this._props.doc.type === DocumentType.COL) { - const filtered_collections = new List<Doc>(DocListCast(this._props.keywordDoc.collections).filter(doc => doc !== this._props.doc)); - this._props.keywordDoc.collections = filtered_collections; - - for (const cur_doc of DocListCast(this.getKeywordCollectionDocs()!, [])) { - this._props.doc[DocData].data = new List<Doc>(DocListCast(this._props.doc[DocData].data).filter(doc => !Doc.AreProtosEqual(cur_doc, doc))); - } - } else { - const filtered_docs = new List<Doc>(DocListCast(this.getKeywordCollectionDocs()!).filter(doc => doc !== this._props.doc)); - this._props.keywordDoc[DocData].docs = filtered_docs; - - for (const collection of DocListCast(this._props.keywordDoc.collections)) { - collection[DocData].data = new List<Doc>(DocListCast(collection[DocData].data).filter(doc => !Doc.AreProtosEqual(this._props.doc, doc))); - } - } - } - this._props.doc[DocData].data_labels = (this._props.doc[DocData].data_labels as List<string>).filter(label => label !== this._props.keyword) as List<string>; - this._props.doc![DocData][`${this._props.keyword}`] = false; - }; - render() { const keyword = this._props.keyword.replace(/^@/, ''); const metadata = this._props.keyword.startsWith('@'); return ( - <div className="keyword" onClick={this._props.setToEditing} onPointerDown={this.handleDragStart} ref={this.ref} key={Utils.GenerateGuid()}> + <div className="keyword" onClick={this._props.setToEditing} onPointerDown={this.handleDragStart} ref={this._ref} key={Utils.GenerateGuid()}> {metadata ? ( <span> <b style={{ fontSize: 'smaller' }}>{keyword} </b> - {this._props.doc[keyword] as string}{' '} + {this._props.doc[keyword] as string} </span> ) : ( keyword )} - {this.props.isEditing && <IconButton tooltip={'Remove label'} onPointerDown={this.removeLabel} icon={'X'} style={{ width: '8px', height: '8px', marginLeft: '10px' }} />} + {this.props.isEditing && ( + <IconButton + tooltip="Remove label" + onPointerDown={undoable(() => KeywordItem.RemoveLabel(this._props.doc, this._props.keyword, this._props.keywordDoc), `remove label ${this._props.keyword}`)} + icon={'X'} + style={{ width: '8px', height: '8px', marginLeft: '10px' }} + /> + )} </div> ); } } interface KeywordBoxProps { - doc: Doc; - isEditing: boolean; + Document: Doc; } /** @@ -152,76 +215,50 @@ interface KeywordBoxProps { */ @observer export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> { - @observable _currentInput: string = ''; - private height: number = 0; - private ref: React.RefObject<HTMLDivElement>; - - @computed - get currentScale() { - return NumCast((this._props.doc.embedContainer as Doc)?._freeform_scale, 1); - } - - @computed - get cur_height() { - return this.ref.current?.offsetHeight ? this.ref.current?.offsetHeight : 0; - } + private _height: number = 0; + private _ref: React.RefObject<HTMLDivElement>; constructor(props: any) { super(props); makeObservable(this); - this.ref = React.createRef(); + this._ref = React.createRef(); reaction( () => this.cur_height, () => { - this._props.doc[DocData].keywordHeight = this.height; + this._props.Document[DocData].keywordHeight = this._height; } ); } - componentDidMount(): void { - this.height = this.ref.current?.offsetHeight ? this.ref.current?.offsetHeight : 0; - this._props.doc[DocData].keywordHeight = this.height; - } + @observable _currentInput = ''; + @observable _isEditing = !StrListCast(this._props.Document[DocData].data_labels).length; - componentDidUpdate(prevProps: Readonly<KeywordBoxProps>): void { - this.height = this.ref.current?.offsetHeight ? this.ref.current?.offsetHeight : 0; - this._props.doc[DocData].keywordHeight = this.height; + @computed get currentScale() { + return NumCast((this._props.Document.embedContainer as Doc)?._freeform_scale, 1); } - @action - setToEditing = () => { - this._props.isEditing = true; - }; + @computed get cur_height() { + return this._ref.current?.offsetHeight ?? 0; + } - @action - setToView = () => { - this._props.isEditing = false; - }; + @computed get isEditing() { + return this._isEditing && SelectionManager.Docs().includes(this._props.Document); + } - /** - * Gets the document associated with a keyword. - * @param keyword The keyword being searched for - * @returns A Doc containing keyword information - */ - getKeywordCollection = (keyword: string) => { - // Look for the keyword document. - for (const doc of DocListCast(Doc.ActiveDashboard!.myKeywordCollections)) { - if (doc.title === keyword) { - return doc; - } - } + componentDidMount() { + this._height = this._ref.current?.offsetHeight ?? 0; + this._props.Document[DocData].keywordHeight = this._height; + } - // If not contained, create a new document and add it to the active Dashboard's keyword list. - const keywordCollection = new Doc(); - keywordCollection.title = keyword; - keywordCollection[DocData].docs = new List<Doc>(); - keywordCollection.collections = new List<Doc>(); - if (Doc.ActiveDashboard) { - Doc.ActiveDashboard.myKeywordCollections = new List<Doc>([...DocListCast(Doc.ActiveDashboard.myKeywordCollections), keywordCollection]); - } + componentDidUpdate(prevProps: Readonly<KeywordBoxProps>): void { + this._height = this._ref.current?.offsetHeight ?? 0; + this._props.Document[DocData].keywordHeight = this._height; + } - return keywordCollection; + @action + setToEditing = () => { + this._isEditing = true; }; /** @@ -229,94 +266,42 @@ export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> { * @param keyword */ submitLabel = undoable((keyword: string) => { - // If the active Dashboard does not have a keyword collection, create it. - if (Doc.ActiveDashboard && !Doc.ActiveDashboard.myKeywordCollections) { - Doc.ActiveDashboard.myKeywordCollections = new List<Doc>(); - } - const submittedLabel = keyword.trim(); - if (submittedLabel && !this._props.doc![DocData][`${submittedLabel}`]) { - // If the keyword collection is not in active Dashboard, add it as a new doc, with the keyword as its title. - const keywordCollection = this.getKeywordCollection(submittedLabel); - - // If the document has no keywords field, create the field. - if (!this._props.doc[DocData].data_labels) { - this._props.doc[DocData].data_labels = new List<string>(); - } - - // If the document is of type COLLECTION, make it a smart collection, otherwise, add the keyword to the document. - if (this._props.doc.type === DocumentType.COL) { - keywordCollection.collections = new List<Doc>([...DocListCast(keywordCollection.collections), this._props.doc]); - - // Iterate through the keyword document's collections and add a copy of the document to each collection - for (const doc of DocListCast(keywordCollection[DocData].docs)) { - const newEmbedding = Doc.MakeEmbedding(doc); - this._props.doc[DocData].data = new List<Doc>([...DocListCast(this._props.doc[DocData].data), newEmbedding]); - newEmbedding.embedContainer = this._props.doc; - } - } else { - // Add this document to the keyword's collection of associated documents. - keywordCollection[DocData].docs = new List<Doc>([...DocListCast(keywordCollection[DocData].docs), this._props.doc]); - - // Iterate through the keyword document's collections and add a copy of the document to each collection - for (const collection of DocListCast(keywordCollection.collections)) { - const newEmbedding = Doc.MakeEmbedding(this._props.doc); - collection[DocData].data = new List<Doc>([...DocListCast(collection.data), newEmbedding]); - newEmbedding.embedContainer = collection; - } - } - - // Push the keyword to the document's keyword list field. - (this._props.doc![DocData].data_labels! as List<string>).push(submittedLabel); - this._props.doc![DocData][`${submittedLabel}`] = true; + if (submittedLabel && !this._props.Document[DocData][submittedLabel]) { + KeywordItem.addLabelToDoc(this._props.Document, submittedLabel); this._currentInput = ''; // Clear the input box } }, 'added doc label'); - @action - onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this._currentInput = e.target.value; - }; - render() { - const keywordsList = this._props.doc[DocData].data_labels ? this._props.doc[DocData].data_labels : new List<string>(); - const seldoc = DocumentView.SelectedDocs().lastElement(); - if (SnappingManager.IsDragging || !(seldoc === this._props.doc) || !this._props.isEditing) { - setTimeout( - action(() => { - if ((keywordsList as List<string>).length === 0) { - this._props.doc[DocData].showLabels = false; - } - this.setToView(); - }) - ); - } + const keywordsList = StrListCast(this._props.Document[DocData].data_labels); - return ( + return !this._props.Document.showLabels ? null : ( <div className="keywords-container" - ref={this.ref} + ref={this._ref} style={{ transformOrigin: 'top left', - overflow: 'hidden', + maxWidth: `${100 * this.currentScale}%`, + width: 'max-content', transform: `scale(${1 / this.currentScale})`, - backgroundColor: this._props.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, - borderColor: this._props.isEditing ? Colors.BLACK : Colors.TRANSPARENT, - maxWidth: `400px`, + backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, + borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT, }}> - <div className="keywords-content"> + <div className="keywords-content" style={{ width: '100%' }}> <div className="keywords-list"> - {(keywordsList as List<string>).map(keyword => { - return <KeywordItem key={Utils.GenerateGuid()} doc={this._props.doc} keyword={keyword} keywordDoc={this.getKeywordCollection(keyword)} setToEditing={this.setToEditing} isEditing={this._props.isEditing}></KeywordItem>; + {keywordsList.map(keyword => { + const keywordDoc = KeywordItem.findKeywordCollectionDoc(keyword); + return !keywordDoc ? null : <KeywordItem key={Utils.GenerateGuid()} doc={this._props.Document} keyword={keyword} keywordDoc={keywordDoc} setToEditing={this.setToEditing} isEditing={this.isEditing} />; })} </div> - {this._props.isEditing ? ( + {this.isEditing ? ( <div className="keyword-editing-box"> <div className="keyword-input-box"> <input value={this._currentInput} autoComplete="off" - onChange={this.onInputChange} + onChange={action(e => (this._currentInput = e.target.value))} onKeyDown={e => { e.key === 'Enter' ? this.submitLabel(this._currentInput) : null; e.stopPropagation(); @@ -328,45 +313,28 @@ export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> { style={{ width: '100%', borderRadius: '5px' }} /> </div> - {Doc.ActiveDashboard?.myKeywordCollections ? ( - <div className="keyword-suggestions-box"> - {DocListCast(Doc.ActiveDashboard?.myKeywordCollections).map(doc => { - const keyword = StrCast(doc.title); - return ( - <Button - style={{ margin: '2px 2px', border: '1px solid black', backgroundColor: 'lightblue', color: 'black' }} - text={keyword} - color={SnappingManager.userVariantColor} - tooltip={'Add existing keyword'} - onClick={() => { - this.submitLabel(keyword); - }} - key={Utils.GenerateGuid()} - /> - ); - })} - </div> - ) : ( - <div></div> - )} + <div className="keyword-suggestions-box"> + {KeywordItem.AllKeywordCollections.map(doc => { + const keyword = StrCast(doc.title); + return ( + <Button + style={{ margin: '2px 2px', border: '1px solid black', backgroundColor: 'lightblue', color: 'black' }} + text={keyword} + color={SnappingManager.userVariantColor} + tooltip={'Add existing keyword'} + onClick={() => this.submitLabel(keyword)} + key={Utils.GenerateGuid()} + /> + ); + })} + </div> <div className="keyword-buttons"> - <IconButton - tooltip={'Close Menu'} - onPointerDown={() => { - if ((keywordsList as List<string>).length === 0) { - this._props.doc[DocData].showLabels = false; - } else { - this.setToView(); - } - }} - icon={'x'} - style={{ width: '4px' }} - /> + {!keywordsList.length ? null : ( // + <IconButton style={{ width: '4px' }} tooltip="Close Menu" onPointerDown={action(() => (this._isEditing = false))} icon="x" /> + )} </div> </div> - ) : ( - <div></div> - )} + ) : null} </div> </div> ); diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 374399445..1e80e7ee5 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -364,13 +364,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & </Tooltip> ); }; - const keywords = () => { - if (doc && doc![DocData].showLabels && (!doc[DocData].data_labels || (doc[DocData].data_labels as List<string>).length === 0)){ - return (<KeywordBox isEditing={true} doc={doc}></KeywordBox>) - } else if (doc && doc![DocData].data_labels && doc![DocData].showLabels) { - return (<KeywordBox isEditing={false} doc={doc}></KeywordBox>) - } - } + const keywords = () => doc ? <KeywordBox Document={doc}/> : null; return ( <> {paint()} |