diff options
| author | bobzel <zzzman@gmail.com> | 2025-04-21 13:48:58 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2025-04-21 13:48:58 -0400 |
| commit | 17e24e780b54f2f7015c0ca955c3aa5091bba19c (patch) | |
| tree | b13002c92d58cb52a02b46e4e1d578f1d57125f2 /src/client/views/nodes | |
| parent | 22a40443193320487c27ce02bd3f134d13cb7d65 (diff) | |
| parent | 1f294ef4a171eec72a069a9503629eaf7975d983 (diff) | |
merged with master and cleaned up outpainting a bit.
Diffstat (limited to 'src/client/views/nodes')
62 files changed, 2097 insertions, 2161 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 25e76e2a6..d4c512342 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -296,7 +296,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { 'Content-Type': 'application/json', }, }); - this.Document[DocData].phoneticTranscription = response.data['transcription']; + this.Document.$phoneticTranscription = response.data['transcription']; }; // context menu @@ -396,7 +396,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { returnFalse, action(() => { const newDoc = DocUtils.GetNewTextDoc('', NumCast(this.Document.x), NumCast(this.Document.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); - const textField = Doc.LayoutFieldKey(newDoc); + const textField = Doc.LayoutDataKey(newDoc); const newDocData = newDoc[DocData]; newDocData[`${textField}_recordingSource`] = this.dataDoc; newDocData[`${textField}_recordingStart`] = ComputedField.MakeFunction(`this.${textField}_recordingSource.${this.fieldKey}_recordingStart`); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index ce1e9280a..940c4cb99 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -17,9 +17,10 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { DocComponent } from '../DocComponent'; import { StyleProp } from '../StyleProp'; import './CollectionFreeFormDocumentView.scss'; -import { DocumentView, DocumentViewProps } from './DocumentView'; +import { DocumentView } from './DocumentView'; import { FieldViewProps } from './FieldView'; import { OpenWhere } from './OpenWhere'; +import { DocumentViewProps } from './DocumentContentsView'; export enum GroupActive { // flags for whether a view is activate because of its relationship to a group group = 'group', // this is a group that is activated because it's on an active canvas, but is not part of some other group @@ -70,7 +71,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF { key: 'freeform_panY' }, ]; // fields that are configured to be animatable using animation frames public static animStringFields = ['backgroundColor', 'borderColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames - public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames + public static animDataFields = (doc: Doc) => (Doc.LayoutDataKey(doc) ? [Doc.LayoutDataKey(doc)] : []); // fields that are configured to be animatable using animation frames public static from(dv?: DocumentView): CollectionFreeFormDocumentView | undefined { return dv?._props.reactParent instanceof CollectionFreeFormDocumentView ? dv._props.reactParent : undefined; } @@ -186,7 +187,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } public static gotoKeyFrame(doc: Doc, newFrame: number) { if (doc) { - const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]); + const childDocs = DocListCast(doc[Doc.LayoutDataKey(doc)]); const currentFrame = Cast(doc._currentFrame, 'number', null); if (currentFrame === undefined) { doc._currentFrame = 0; @@ -304,7 +305,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF ) : ( <DocumentView {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore - Document={this._props.Document} + Document={this.Document} renderDepth={this._props.renderDepth} isContentActive={this._props.isContentActive} childFilters={this._props.childFilters} diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index c0c6db4d3..6d5891003 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -29,7 +29,6 @@ import '../pdf/GPTPopup/GPTPopup.scss'; import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { TraceMobx } from '../../../fields/util'; const API_URL = 'https://api.unsplash.com/search/photos'; @@ -80,8 +79,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const front = Docs.Create.CenteredTextCreator('question', question, {}, img); const back = Docs.Create.CenteredTextCreator('answer', answer, {}); if (useDoc) { - useDoc[DocData][frontKey] = front; - useDoc[DocData][backKey] = back; + useDoc['$' + frontKey] = front; + useDoc['$' + backKey] = back; return useDoc; } return Docs.Create.FlashcardDocument(title, front, back, { _width: 300, _height: 300 }); @@ -285,13 +284,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }; @action handleRenderGPTClick = () => { - const phonTrans = DocCast(this.Document.audio) ? DocCast(this.Document.audio).phoneticTranscription : undefined; - if (phonTrans) { - this._inputValue = StrCast(phonTrans); - this.askGPTPhonemes(this._inputValue); - this._renderSide = this.backKey; - this._outputValue = ''; - } else if (this._inputValue) this.askGPT(GPTCallType.QUIZDOC); + if (this._inputValue) this.askGPT(GPTCallType.QUIZDOC); }; onPointerMove = ({ movementX }: PointerEvent) => { @@ -468,45 +461,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }; /** - * Gets the transcription of an audio recording by sending the - * recording to backend. - */ - pushInfo = () => - axios - .post( - 'http://localhost:105/recognize/', // - { file: DocCast(this.Document.audio)[DocData].url }, - { headers: { 'Content-Type': 'application/json' } } - ) - .then(response => { - this.Document.phoneticTranscription = response.data.transcription; - }); - - /** - * Extracts the id of the youtube video url. - * @param url - * @returns - */ - getYouTubeVideoId = (url: string) => { - const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=|\?v=)([^#&?]*).*/; - const match = url.match(regExp); - return match && match[2].length === 11 ? match[2] : null; - }; - - /** - * Gets the transcript of a youtube video by sending the video url to the backend. - * @returns transcription of youtube recording - */ - youtubeUpload = async () => - axios - .post( - 'http://localhost:105/youtube/', // - { file: this.getYouTubeVideoId(this.frontText) }, - { headers: { 'Content-Type': 'application/json' } } - ) - .then(response => response.data.transcription); - - /** * Calls GPT for each flashcard type. */ askGPT = async (callType: GPTCallType) => { @@ -520,7 +474,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() action(resp => { switch (resp && callType) { case GPTCallType.CHATCARD: - DocCast(this.dataDoc[this.backKey])[DocData].text = resp; + DocCast(this.dataDoc[this.backKey]).$text = resp; break; case GPTCallType.QUIZDOC: this._renderSide = this.backKey; @@ -540,45 +494,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() layoutHeight = () => NumCast(this.layoutDoc.height, 200); /** - * Ask GPT for advice on how to improve speech by comparing the phonetic transcription of - * a users audio recording with the phonetic transcription of their intended sentence. - * @param phonemes - */ - askGPTPhonemes = async (phonemes: string) => { - const sentence = this.frontText; - const phon6 = 'huː ɑɹ juː tədeɪ'; - const phon4 = 'kamo estas hɔi'; - const promptEng = - 'Consider all possible phonetic transcriptions of the intended sentence "' + - sentence + - '" that is standard in American speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + - phon6 + - '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in American speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Only note the difference if they are not allophones of the same phoneme and if they are far away on the vowel chart. The goal is to be understood, not sound like a native speaker. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Provide a response like this: "Lets work on improving the pronunciation of "coffee." You said "ceeffee," which is close, but we need to adjust the vowel sound. In American English, "coffee" is pronounced /ˈkɔːfi/, with a long "aw" sound. Try saying "kah-fee." Your intonation is good, but try putting a bit more stress on "like" in the sentence "I would like a coffee with milk." This will make your speech sound more natural. Keep practicing, and lets try saying the whole sentence again!"'; - const promptSpa = - 'Consider all possible phonetic transcriptions of the intended sentence "' + - 'como estás hoy' + - '" that is standard in Spanish speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + - phon4 + - '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in Spanish speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Only note the difference if they are not allophones of the same phoneme and if they are far away on the vowel chart; say good job if it would be understood by a native Spanish speaker. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". Do not make "θ" and "f" interchangable. Do not make "n" and "ɲ" interchangable. Do not make "e" and "i" interchangable. If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Identify "ɔi" sounds like "oy". Ignore accents and do not say anything to the user about this.'; - const promptAll = - 'Consider all possible phonetic transcriptions of the intended sentence "' + - sentence + - '" that is standard in ' + - this.convertAbr() + - ' speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + - phonemes + - '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in ' + - this.convertAbr() + - ' speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". Do not make "θ" and "f" interchangable. Do not make "n" and "ɲ" interchangable. Do not make "e" and "i" interchangable. If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Provide a response like this: "Lets work on improving the pronunciation of "coffee." You said "cawffee," which is close, but we need to adjust the vowel sound. In American English, "coffee" is pronounced /ˈkɔːfi/, with a long "aw" sound. Try saying "kah-fee." Your intonation is good, but try putting a bit more stress on "like" in the sentence "I would like a coffee with milk." This will make your speech sound more natural. Keep practicing, and lets try saying the whole sentence again!"'; - - switch (this._recognition.lang) { - case 'en-US': this._outputValue = await gptAPICall(promptEng, GPTCallType.PRONUNCIATION); break; - case 'es-ES': this._outputValue = await gptAPICall(promptSpa, GPTCallType.PRONUNCIATION); break; - default: this._outputValue = await gptAPICall(promptAll, GPTCallType.PRONUNCIATION); break; - } // prettier-ignore - }; - - /** * Display a user's speech to text result. * @param e */ @@ -618,7 +533,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const hrefBase64 = await imageUrlToBase64(u); const response = await gptImageLabel(hrefBase64, 'Answer the following question as a short flashcard response. Do not include a label.' + (this.dataDoc.text as RichTextField)?.Text); - DocCast(this.dataDoc[this.backKey])[DocData].text = response; + DocCast(this.dataDoc[this.backKey]).$text = response; } catch (error) { console.log('Error', error); } @@ -637,31 +552,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); }; - testForTextFields = (whichSlot: string) => { - const slotData = Doc.Get(this.dataDoc, whichSlot, true); - const slotHasText = slotData instanceof RichTextField || typeof slotData === 'string'; - const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim(); - const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim(); - const layoutTemplateString = - slotHasText ? FormattedTextBox.LayoutString(whichSlot): - whichSlot === this.frontKey ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) : - altText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore - - // A bit hacky to try out the concept of using GPT to fill in flashcards - // If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string) - // and the fieldKey + "_alternate" has text that includes a GPT query (indicated by (( && )) ) that is parameterized (optionally) by the fieldKey text (this) or other metadata (this.<field>). - // eg., this.text_alternate is - // "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))" - // where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field - // The GPT call will put the "answer" in the second slot of the comparison (eg., text_0) - if (whichSlot === this.backKey && !layoutTemplateString?.includes(whichSlot)) { - const queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in Doc.setField but it doesn't know about the fieldKey ... - if (queryText?.match(/\(\(.*\)\)/)) { - Doc.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt - } - } - return layoutTemplateString; - }; childActiveFunc = () => this._childActive; contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); @@ -682,24 +572,21 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() displayDoc = (whichSlot: string) => { const whichDoc = DocCast(this.dataDoc[whichSlot]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); - return targetDoc || layoutString ? ( + return targetDoc ? ( <> <DocumentView {...this._props} - Document={layoutString ? this.Document : targetDoc} + Document={targetDoc} NativeWidth={returnZero} NativeHeight={returnZero} renderDepth={this.props.renderDepth + 1} - LayoutTemplateString={layoutString} containerViewPath={this._props.docViewPath} ScreenToLocalTransform={this.contentScreenToLocalXf} isDocumentActive={returnFalse} isContentActive={this.childActiveFunc} showTags={undefined} fitWidth={this.childFitWidth} // set to returnTrue to make images fill the comparisonBox-- should be a user option - ignoreUsePath={layoutString ? true : undefined} moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack} removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack} dontSelect={returnTrue} @@ -745,18 +632,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() </div> <div> <div className="submit-button"> - {/* <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, false)}> - <FontAwesomeIcon color="white" icon="caret-down" /> - </div> */} - {/* <button className="submit-buttonrecord" onClick={this._listening ? this.stopListening : this.startListening} style={{ background: this._listening ? 'lightgray' : '' }}> - {<FontAwesomeIcon icon="microphone" size="lg" />} - </button> */} - {/* <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, true)} style={{ left: '50px', zIndex: '100' }}> - <FontAwesomeIcon color="white" icon="caret-down" /> - </div> */} - {/* <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}> - Evaluate Pronunciation - </button> */} <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.backKey ? () => this.animateFlipping(this.frontKey) : this.handleRenderGPTClick}> {this._renderSide === this.backKey ? 'Redo the Question' : 'Submit'} </button> diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index d5e37b3b5..c80e8ebc1 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,43 +1,39 @@ +import { Colors, Toggle, ToggleType, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox } from '@mui/material'; -import { Colors, Toggle, ToggleType, Type } from '@dash/components'; import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { ClientUtils, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils'; +import { returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; -import { Doc, DocListCast, Field, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { PrefetchProxy } from '../../../../fields/Proxy'; import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; -import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; +import { TraceMobx } from '../../../../fields/util'; import { GPTCallType, gptAPICall } from '../../../apis/gpt/GPT'; import { DocUtils } from '../../../documents/DocUtils'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; -import { LinkManager } from '../../../util/LinkManager'; import { UndoManager, undoable } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { MarqueeAnnotator } from '../../MarqueeAnnotator'; import { PinProps } from '../../PinFuncs'; import { SidebarAnnos } from '../../SidebarAnnos'; -import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup'; import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { FocusViewOptions } from '../FocusViewOptions'; import './DataVizBox.scss'; -import { Col, DataVizTemplateInfo, DocCreatorMenu, LayoutType} from './DocCreatorMenu/DocCreatorMenu'; +import { Col, DocCreatorMenu } from './DocCreatorMenu/DocCreatorMenu'; +import { TemplateFieldSize, TemplateFieldType } from './DocCreatorMenu/TemplateBackend'; import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; -import { TemplateFieldSize, TemplateFieldType } from './DocCreatorMenu/TemplateBackend'; export enum DataVizView { TABLE = 'table', @@ -356,7 +352,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const getFrom = DocCast(this.layoutDoc.dataViz_asSchema); if (!getFrom?.schema_columnKeys) return undefined; const keys = StrListCast(getFrom?.schema_columnKeys).filter(key => key !== 'text'); - const children = DocListCast(getFrom?.[Doc.LayoutFieldKey(getFrom)]); + const children = DocListCast(getFrom?.[Doc.LayoutDataKey(getFrom)]); const current: { [key: string]: string }[] = []; children .filter(child => child) @@ -440,10 +436,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; if (!this.records.length) return 'no data/visualization'; switch (this.dataVizView) { - case DataVizView.TABLE: return <TableBox {...sharedProps} specHighlightedRow={this._specialHighlightedRow} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>; - case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} vizBox={this} />; - case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} />; - case DataVizView.PIECHART: return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} + case DataVizView.TABLE: return <TableBox {...sharedProps} Doc={this.Document} specHighlightedRow={this._specialHighlightedRow} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>; + case DataVizView.LINECHART: return <LineChart {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} vizBox={this} />; + case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} />; + case DataVizView.PIECHART: return <PieChart {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} margin={{ top: 10, right: 15, bottom: 15, left: 15 }} />; default: } // prettier-ignore @@ -574,9 +570,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const cols = Array.from(Object.keys(this.records[0])).filter(header => header !== '' && header !== undefined); - cols.forEach(col => { - this.setColumnDefault(col, `${this.records[rowToCheck][col]}`); - }); + cols.forEach(col => this.setColumnDefault(col, `${this.records[rowToCheck][col]}`)); }; updateGPTSummary = async () => { @@ -706,7 +700,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ref={this._sidebarRef} {...this._props} fieldKey={this.fieldKey} - Document={this.Document} + Doc={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} usePanelWidth diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index 5a9442d2f..5450d03b1 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -1,27 +1,29 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ColorPicker, EditableText, IconButton, Size, Type } from '@dash/components'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import * as d3 from 'd3'; -import { IReactionDisposer, action, computed, makeObservable, observable } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaFillDrip } from 'react-icons/fa'; -import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc'; +import { Doc, NumListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; import { undoable } from '../../../../util/UndoManager'; import { ObservableReactComponent } from '../../../ObservableReactComponent'; -import { PinProps, PinDocView } from '../../../PinFuncs'; +import { PinDocView, PinProps } from '../../../PinFuncs'; import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils'; import './Chart.scss'; +export interface HistogramData { + [key: string]: string | number; +} export interface HistogramProps { - Document: Doc; + Doc: Doc; layoutDoc: Doc; axes: string[]; - titleCol: string; - records: { [key: string]: any }[]; + records: HistogramData[]; width: number; height: number; dataDoc: Doc; @@ -39,64 +41,85 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _histogramRef: HTMLDivElement | null = null; private _histogramSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; - private numericalXData: boolean = false; // whether the data is organized by numbers rather than categoreis - private numericalYData: boolean = false; // whether the y axis is controlled by provided data rather than frequency - private maxBins = 15; // maximum number of bins that is readable on a normal sized doc - @observable _currSelected: any | undefined = undefined; // Object of selected bar - private curBarSelected: any = undefined; // histogram bin of selected bar for when just one bar is selected - private selectedData: any[] = []; // array of selected bars - private hoverOverData: any = undefined; // Selection of bar being hovered over - - constructor(props: any) { + private _numericalXData: boolean = false; // whether the data is organized by numbers rather than categoreis + private _numericalYData: boolean = false; // whether the y axis is controlled by provided data rather than frequency + private _maxBins = 15; // maximum number of bins that is readable on a normal sized doc + private _selectedBars: HistogramData[] = []; + @observable private _currSelected: { [key: string]: string | number; frequency: number } | undefined = undefined; + + constructor(props: HistogramProps) { super(props); makeObservable(this); } + @computed get xAxis() { + return this._props.axes[0]; + } + + @computed get yAxis() { + return this._props.axes[1]; + } + + @computed get Doc() { + return this._props.Doc; + } + @computed get layoutDoc() { + return this._props.layoutDoc; + } + @computed get _tableDataIds() { return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) - @computed get _tableData() { + + @computed get _tableData(): Record<string, string | number>[] { return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } - // filters all data to just display selected data if brushed (created from an incoming link) - @computed get _histogramData() { + + @computed get _histogramData(): HistogramData[] { if (this._props.axes.length < 1) return []; if (this._props.axes.length < 2) { - const ax0 = this._props.axes[0]; - if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) { - this.numericalXData = true; + if (!/[A-Za-z-:]/.test(this._props.records[0][this.xAxis] as string)) { + this._numericalXData = true; } - return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] })); + return this._tableData.map(record => ({ [this.xAxis]: record[this.xAxis] })); } - const [ax0, ax1] = this._props.axes; - if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) { - this.numericalXData = true; + if (!/[A-Za-z-:]/.test(this._props.records[0][this.xAxis] as string)) { + this._numericalXData = true; } - if (!/[A-Za-z-:]/.test(this._props.records[0][ax1])) { - this.numericalYData = true; + if (!/[A-Za-z-:]/.test(this._props.records[0][this.yAxis] as string)) { + this._numericalYData = true; } - return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]], [ax1]: record[this._props.axes[1]] })); + return this._tableData.map(record => ({ + [this.xAxis]: record[this.xAxis], + [this.yAxis]: record[this.yAxis], + })); } - @computed get defaultGraphTitle() { - const [ax0, ax1] = this._props.axes; - if (this._props.axes.length < 2 || !ax1 || !/\d/.test(this._props.records[0][ax1]) || !this.numericalYData) { - return ax0 + ' Histogram'; + @computed get defaultGraphTitle(): string { + if (!this.yAxis || !/\d/.test(this._props.records[0][this.yAxis] as string) || !this._numericalYData) { + return this.xAxis + ' Histogram'; } - return ax0 + ' by ' + ax1 + ' Histogram'; + return this.xAxis + ' by ' + this.yAxis + ' Histogram'; } @computed get parentViz() { - return DocCast(this._props.Document.dataViz_parentViz); + return DocCast(this._props.Doc.dataViz_parentViz); + } + + @computed get defaultBarColor() { + return Cast(this.layoutDoc.dataViz_histogram_defaultColor, 'string', '#69b3a2'); + } + @computed get barColors() { + return Cast(this.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); + } + @computed get selectedBins() { + return Cast(this.layoutDoc.dataViz_histogram_selectedBins, listSpec('number'), null); } @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { - if (this.numericalXData) { - const data = this.data(this._histogramData); - return { xMin: Math.min.apply(null, data), xMax: Math.max.apply(null, data), yMin: 0, yMax: 0 }; - } - return { xMin: 0, xMax: 0, yMin: 0, yMax: 0 }; + const data = this._numericalXData ? this.data(this._histogramData) : [0]; + return { xMin: Math.min(...data), xMax: Math.max(...data), yMin: 0, yMax: 0 }; } componentWillUnmount() { @@ -104,21 +127,31 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } componentDidMount() { // restore selected bars - const svg = this._histogramSvg; - if (svg) { - const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_histogram_selectedData); - svg.selectAll('rect').attr('class', (d: any) => { - let selected = false; - selectedDataBars.forEach(eachSelectedData => { - if (d[0] === eachSelectedData) selected = true; - }); - if (selected) { - this.selectedData.push(d); - return 'histogram-bar hover'; - } - return 'histogram-bar'; - }); - } + this._histogramSvg?.selectAll('rect').attr('class', dIn => { + const d = dIn as HistogramData; + if (this.selectedBins.some(selBin => d[0] === selBin)) { + this._selectedBars.push(d); + return 'histogram-bar hover'; + } + return 'histogram-bar'; + }); + // setup filters to watch selections and filter toggle + this._disposers.selection = reaction( + () => ({ filter: this.layoutDoc.dataViz_filterSelection, hists: this._selectedBars.slice(), cur: this._currSelected }), + ({ filter, hists }) => { + this.layoutDoc.dataViz_selectedRows = !filter + ? undefined + : new List<number>( + this._tableDataIds.filter(rowID => + hists.some(h => { + const val = this._props.records[rowID][this.xAxis]; + return val == h.x0 || (+val >= +h.x0 && +val <= +h.x1); + }) + ) + ); + }, + { fireImmediately: true } + ); } // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) @@ -126,7 +159,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { const anchor = Docs.Create.ConfigDocument({ title: 'histogram doc selection' + this._currSelected, }); - PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Doc); return anchor; }; @@ -139,110 +172,92 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } // cleans data by converting numerical data to numbers and taking out empty cells - data = (dataSet: any) => { - const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as any))); + data = (dataSet: HistogramData[]): number[] => { + const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as number))); const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; return !field ? [] - : validData.map((d: { [x: string]: any }) => - !this.numericalXData // - ? d[field] - : +d[field!].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') + : validData.map(d => + !this._numericalXData // + ? (d[field] as number) + : +d[field].toString().replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') ); }; + barLabel = (d: d3.Bin<number, number> | HistogramData) => '' + (Array.isArray(d) ? d[0] : d[0]); + // outlines the bar selected / hovered over - highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => { + highlightSelectedBar = (changeSelectedVariables: boolean, svg: d3.Selection<SVGGElement, unknown, null, undefined>, eachRectWidth: number, pointerX: number, xAxisTitle: string, yAxisTitle: string, histDataSet: HistogramData[]) => { let barCounter = -1; - const selected = svg.selectAll('.histogram-bar').filter((d: any) => { + let hoverOverBar: HistogramData | undefined; + svg.selectAll('.histogram-bar').filter(dIn => { + const d = dIn as HistogramData; barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over - if (d.length && barCounter * eachRectWidth <= pointerX && pointerX <= (barCounter + 1) * eachRectWidth) { - let showSelected = this.numericalYData - ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0] - : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0]; - if (this.numericalXData) { - // calculating frequency - if (d[0] && d[1] && d[0] !== d[1]) { - showSelected = { [xAxisTitle]: d3.min(d) + ' to ' + d3.max(d), frequency: d.length }; - } else if (!this.numericalYData) showSelected = { [xAxisTitle]: showSelected[xAxisTitle], frequency: d.length }; - } + if (d.length && (barCounter * eachRectWidth <= pointerX + 1 || (!barCounter && pointerX <= 0)) && pointerX - 1 <= (barCounter + 1) * eachRectWidth) { if (changeSelectedVariables) { // for when a bar is selected - not just hovered over - let sameAsAny = false; - const selectedDataBars = Cast(this._props.layoutDoc.dataViz_histogram_selectedData, listSpec('number'), null); - this.selectedData.forEach(eachData => { - if (!sameAsAny) { - let match = true; - Object.keys(d).forEach(key => { - if (d[key] !== eachData[key]) match = false; - }); - if (match) { - sameAsAny = true; - const index = this.selectedData.indexOf(eachData); - this.selectedData.splice(index, 1); - selectedDataBars.splice(index, 1); - this._currSelected = undefined; - } - } - }); - if (!sameAsAny) { - this.selectedData.push(d); - selectedDataBars.push(d[0]); - this._currSelected = this.selectedData.length > 1 ? undefined : showSelected; + const alreadySelected = this._selectedBars.findIndex(eachData => !Object.keys(d).some(key => d[key] !== eachData[key])); + if (alreadySelected !== -1) { + this._selectedBars.splice(alreadySelected, 1); + this.selectedBins.splice(alreadySelected, 1); + } else { + this._selectedBars.push(d); + this.selectedBins.push(d[0] as number); } + const showSelectedLabel = (dataset: HistogramData[]) => { + const datum = dataset.lastElement(); + const datumNum = datum as unknown as number[]; + const showSelectedStart = this._numericalYData + ? this._histogramData.filter(data => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0] + : histDataSet.filter(data => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0]; - // for filtering child dataviz docs - if (this._props.layoutDoc.dataViz_filterSelection) { - const selectedRows = Cast(this._props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); - this._tableDataIds.forEach(rowID => { - let match = false; - for (let i = 0; i < d.length; i++) { - console.log('Compare: ' + this._props.records[rowID][xAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') + ' = ' + d[i]); - if (this._props.records[rowID][xAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[i]) match = true; - } - if (match && !selectedRows?.includes(rowID)) - selectedRows?.push(rowID); // adding to filtered rows - else if (match && sameAsAny) selectedRows.splice(selectedRows.indexOf(rowID), 1); // removing from filtered rows - }); - } - } else this.hoverOverData = d; + const selectionLabel = dataset.length > 1 + ? dataset.map(dd => this.barLabel(dd)).join('::') + : !this._numericalXData + ? this.barLabel(d) + : datum[0] !== undefined && datum[1] && datum[0] !== datum[1] + ? d3.min(datumNum) + ' to ' + d3.max(datumNum) + : !this._numericalYData + ? showSelectedStart?.[xAxisTitle] + : this.barLabel(d); // prettier-ignore + return { [xAxisTitle]: selectionLabel, frequency: dataset.length > 1 ? Number.NaN : datum.length } as { [key: string]: string | number; frequency: number }; + }; + this._currSelected = this._selectedBars.length ? showSelectedLabel(this._selectedBars) : undefined; + } else hoverOverBar = d; return true; } return false; }); - if (changeSelectedVariables) { - if (this._currSelected) this.curBarSelected = selected; - else this.curBarSelected = undefined; - } + return hoverOverBar; }; // draws the histogram - drawChart = (dataSet: any, width: number, height: number) => { + drawChart = (dataSet: HistogramData[], width: number, height: number) => { if (dataSet?.length <= 0) return; d3.select(this._histogramRef).select('svg').remove(); d3.select(this._histogramRef).select('.tooltip').remove(); const data = this.data(dataSet); const xAxisTitle = Object.keys(dataSet[0])[0]; - const yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; + const yAxisTitle = this._numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; const uniqueArr: unknown[] = [...new Set(data)]; - let numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length; - let translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0; - if (numBins > this.maxBins) numBins = this.maxBins; - const startingPoint = this.numericalXData ? this.rangeVals.xMin! : 0; - const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins; + let numBins = this._numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length; + let translateXAxis = !this._numericalXData || numBins < this._maxBins ? width / (numBins + 1) / 2 : 0; + if (numBins > this._maxBins) numBins = this._maxBins; + const startingPoint = this._numericalXData ? this.rangeVals.xMin! : 0; + const endingPoint = this._numericalXData ? this.rangeVals.xMax! : numBins; // converts data into Objects - let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as any))); - if (!this.numericalXData) { - const histStringDataSet: { [x: string]: unknown }[] = []; - if (this.numericalYData) { + let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as number))); + if (!this._numericalXData) { + const histStringDataSet: { [x: string]: number }[] = []; + if (this._numericalYData) { for (let i = 0; i < dataSet.length; i++) { - histStringDataSet.push({ [yAxisTitle]: dataSet[i][yAxisTitle], [xAxisTitle]: dataSet[i][xAxisTitle] }); + histStringDataSet.push({ [yAxisTitle]: dataSet[i][yAxisTitle] as number, [xAxisTitle]: dataSet[i][xAxisTitle] as number }); } } else { for (let i = 0; i < uniqueArr.length; i++) { - histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] }); + histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] as number }); } for (let i = 0; i < data.length; i++) { const barData = histStringDataSet.filter(each => each[xAxisTitle] == data[i]); @@ -263,12 +278,12 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { .attr('transform', 'translate(' + this._props.margin.left + ',' + this._props.margin.top + ')')); let x = d3 .scaleLinear() - .domain(this.numericalXData ? [startingPoint!, endingPoint!] : [0, numBins]) + .domain(this._numericalXData ? [startingPoint!, endingPoint!] : [0, numBins]) .range([0, width]); const histogram = d3 - .histogram() + .bin() .value(d => d) - .domain([startingPoint!, endingPoint!]) + .domain([startingPoint, endingPoint]) .thresholds(x.ticks(numBins)); const bins = histogram(data); let eachRectWidth = width / bins.length; @@ -279,7 +294,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { // more calculations based on bins // x-axis - if (!this.numericalXData) { + if (!this._numericalXData) { // reorganize to match data if the data is strings rather than numbers // uniqueArr.sort() histDataSet.sort(); @@ -294,9 +309,6 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } bins.pop(); eachRectWidth = width / bins.length; - bins.forEach(d => { - d.x0 = d.x0!; - }); xAxis = d3 .axisBottom(x) .ticks(bins.length > 1 ? bins.length - 1 : 1) @@ -329,15 +341,15 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { x.range([0, width - eachRectWidth]); } // y-axis - const maxFrequency = this.numericalYData ? - d3.max(histDataSet, (d: any) => (d[yAxisTitle] ? Number(d[yAxisTitle]!.replace(/\$/g, '') - .replace(/%/g, '').replace(/</g, '')) : 0)) : + const maxFrequency = this._numericalYData ? + d3.max(histDataSet, d => (d[yAxisTitle] ? + Number(StrCast(d[yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')) : 0)) : d3.max(bins, d => d.length); // prettier-ignore const y = d3.scaleLinear().range([height, 0]); - y.domain([0, +maxFrequency!]); + y.domain([0, maxFrequency ?? 0]); const yAxis = d3.axisLeft(y).ticks(maxFrequency!); - if (this.numericalYData) { - const yScale = scaleCreatorNumerical(0, Number(maxFrequency), height, 0); + if (this._numericalYData) { + const yScale = scaleCreatorNumerical(0, maxFrequency ?? 0, height, 0); yAxisCreator(svg.append('g'), width, yScale); } else { svg.append('g').call(yAxis); @@ -347,29 +359,14 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { .call(xAxis); // click/hover + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateHighlights = (hoverOverBar?: HistogramData) => svg.selectAll('rect').attr('class', (d: any) => 'histogram-bar' + (hoverOverBar?.[0] == d[0] || this._selectedBars.some(hist => d[0] === hist[0]) ? ' hover' : '')); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)); - const onHover = action((e: any) => { - this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet); - // eslint-disable-next-line no-use-before-define - updateHighlights(); - }); - const mouseOut = action(() => { - this.hoverOverData = undefined; - // eslint-disable-next-line no-use-before-define - updateHighlights(); - }); - const updateHighlights = () => { - const hoverOverBar = this.hoverOverData; - const { selectedData } = this; - svg.selectAll('rect').attr('class', (d: any) => { - let selected = false; - selectedData.forEach(eachSelectedData => { - if (d[0] === eachSelectedData[0]) selected = true; - }); - return (hoverOverBar && hoverOverBar[0] == d[0]) || selected ? 'histogram-bar hover' : 'histogram-bar'; - }); - }; - svg.on('click', onPointClick).on('mouseover', onHover).on('mouseout', mouseOut); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mouseEnter = (e: any) => updateHighlights(this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)); + svg.on('click', onPointClick).on('pointerenter', mouseEnter).on('pointerleave', updateHighlights); // axis titles svg.append('text') @@ -385,138 +382,54 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { d3.format('.0f'); // draw bars - const selected = this.selectedData; + const selected = this._selectedBars; svg.selectAll('rect') .data(bins) .enter() .append('rect') - .attr( - 'transform', - this.numericalYData + .attr('transform', this._numericalYData ? d => { - const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] == d[0]); + const eachData = histDataSet.filter((hData: HistogramData) => hData[xAxisTitle] == d[0]); const length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; return 'translate(' + x(d.x0!) + ',' + y(Number(length)) + ')'; } - : d => 'translate(' + x(d.x0!) + ',' + y(d.length) + ')' - ) - .attr( - 'height', - this.numericalYData + : d => 'translate(' + x(d.x0!) + ',' + y(d.length) + ')') + .attr('height', this._numericalYData ? d => { - const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] == d[0]); + const eachData = histDataSet.filter(hData => hData[xAxisTitle] == d[0]); const length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; - return height - y(Number(length)); + return height - y(+length); } - : d => height - y(d.length) - ) + : d => height - y(d.length)) .attr('width', eachRectWidth) - .attr('class', selected ? d => (selected && selected[0] == d[0] ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar') - .attr('fill', d => { - let barColor; - const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); - barColors.forEach(each => { - // eslint-disable-next-line prefer-destructuring - if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; - else { - const range = StrCast(each[0]).split(' to '); - // eslint-disable-next-line prefer-destructuring - if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; - } - }); - return barColor ? StrCast(barColor) : StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor); - }); - }; - - @action changeSelectedColor = (color: string) => { - this.curBarSelected.attr('fill', color); - const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); - - const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); - barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); - barColors.push(StrCast(barName + '::' + color)); + .attr('class', selected ? d => (selected?.[0]?.x0 == d.x0 ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar') + .attr('fill', d => this.barColors?.map(bar => bar.split('::')).find(([barLabel]) => barLabel === this.barLabel(d))?.[1] ?? this.defaultBarColor); // prettier-ignore }; - @action eraseSelectedColor = () => { - this.curBarSelected.attr('fill', this._props.layoutDoc.dataViz_histogram_defaultColor); - const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); - - const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); - barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); - }; - - // reloads the bar colors and selected bars - updateSavedUI = () => { - const svg = this._histogramSvg; - if (svg) { - // bar color - svg.selectAll('rect').attr('fill', (d: any) => { - let barColor; - const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); - barColors.forEach(each => { - // eslint-disable-next-line prefer-destructuring - if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; - else { - const range = StrCast(each[0]).split(' to '); - // eslint-disable-next-line prefer-destructuring - if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; - } - }); - return barColor ? StrCast(barColor) : StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor); - }); - } + @action changeSelectedColor = (color: string, erase?: boolean) => { + if (!this.barColors) this.layoutDoc.dataViz_histogram_barColors = new List<string>(); + this._selectedBars.map(this.barLabel).forEach(barName => { + this.barColors.forEach(bar => bar.split('::')[0] === barName && this.barColors.splice(this.barColors.indexOf(bar), 1)); + !erase && this.barColors.push(barName + '::' + color); + }); }; render() { - this.updateSavedUI(); - this._histogramData; - let curSelectedBarName = ''; - let titleAccessor: any = 'dataViz_histogram_title'; - if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; - else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; - if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; - if (!this._props.layoutDoc.dataViz_histogram_defaultColor) this._props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2'; - if (!this._props.layoutDoc.dataViz_histogram_barColors) this._props.layoutDoc.dataViz_histogram_barColors = new List<string>(); - if (!this._props.layoutDoc.dataViz_histogram_selectedData) this._props.layoutDoc.dataViz_histogram_selectedData = new List<string>(); - let selected = 'none'; - if (this._currSelected) { - curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); - selected = '{ '; - Object.keys(this._currSelected).forEach(key => { - key // - ? (selected += key + ': ' + this._currSelected[key] + ', ') - : ''; - }); - selected = selected.substring(0, selected.length - 2) + ' }'; - if (this._props.titleCol !== '' && (!this._currSelected.frequency || this._currSelected.frequency < 10)) { - selected += '\n' + this._props.titleCol + ': '; - this._tableData.forEach(each => { - if (this._currSelected[this._props.axes[0]] === each[this._props.axes[0]]) { - if (this._props.axes[1]) { - if (this._currSelected[this._props.axes[1]] === each[this._props.axes[1]]) selected += each[this._props.titleCol] + ', '; - } else selected += each[this._props.titleCol] + ', '; - } - }); - selected = selected.slice(0, -1).slice(0, -1); - } - } - let selectedBarColor; - const barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::')); - barColors.forEach(each => { - // eslint-disable-next-line prefer-destructuring - each[0] === curSelectedBarName && (selectedBarColor = each[1]); - }); + if (!this.selectedBins) this.layoutDoc.dataViz_histogram_selectedBins = new List<string>(); + + const titleAccessor = 'dataViz_histogram_title ' + this.xAxis + (this.yAxis ? '-' + this._props.axes[1] : ''); + const selected = !this._currSelected ? 'none' : '{ ' + Object.keys(this._currSelected).map(key => key ? key + ': ' + this._currSelected?.[key]:'').join(", ") + ' }'; // prettier-ignore + const curSelectedBarName = this._selectedBars.length && this.barLabel(this._selectedBars.lastElement()); //.[this.xAxis]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, ''); + const selectedBarColor = this.barColors?.map(bar => bar.split('::'))?.find(([barLabel]) => barLabel === curSelectedBarName)?.[1]; if (this._histogramData.length > 0 || !this.parentViz) { return this._props.axes.length >= 1 ? ( <div className="chart-container" style={{ width: this._props.width + this._props.margin.right }}> <div className="graph-title"> <EditableText - val={StrCast(this._props.layoutDoc[titleAccessor])} + val={StrCast(this.layoutDoc[titleAccessor], this.defaultGraphTitle)} setVal={undoable( - action(val => { - this._props.layoutDoc[titleAccessor] = val as string; - }), + action(val => (this.layoutDoc[titleAccessor] = val)), 'Change Graph Title' )} color="black" @@ -528,13 +441,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { tooltip="Change Default Bar Color" type={Type.SEC} icon={<FaFillDrip />} - selectedColor={StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor)} - setFinalColor={undoable(color => { - this._props.layoutDoc.dataViz_histogram_defaultColor = color; - }, 'Change Default Bar Color')} - setSelectedColor={undoable(color => { - this._props.layoutDoc.dataViz_histogram_defaultColor = color; - }, 'Change Default Bar Color')} + selectedColor={this.defaultBarColor} + setFinalColor={undoable(color => (this.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} + setSelectedColor={undoable(color => (this.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} size={Size.XSMALL} /> </div> @@ -552,9 +461,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { tooltip="Change Bar Color" type={Type.SEC} icon={<FaFillDrip />} - selectedColor={selectedBarColor || this.curBarSelected.attr('fill')} - setFinalColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')} - setSelectedColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')} + selectedColor={selectedBarColor} + setFinalColor={undoable(this.changeSelectedColor, 'Change Selected Bar Color')} + setSelectedColor={undoable(this.changeSelectedColor, 'Change Selected Bar Color')} size={Size.XSMALL} /> @@ -564,7 +473,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { color="black" type={Type.SEC} tooltip="Revert to the default bar color" // - onClick={undoable(this.eraseSelectedColor, 'Change Selected Bar Color')} + onClick={undoable(() => this.changeSelectedColor(this.defaultBarColor, true), 'Change Selected Bar Color')} /> </div> ) : null} diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index b55d509ff..6b047546c 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -22,7 +22,7 @@ export interface SelectedDataPoint extends DataPoint { } export interface LineChartProps { vizBox: DataVizBox; - Document: Doc; + Doc: Doc; layoutDoc: Doc; axes: string[]; titleCol: string; @@ -53,7 +53,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } @computed get titleAccessor() { - let titleAccessor: any = 'dataViz_lineChart_title'; + let titleAccessor = 'dataViz_lineChart_title'; if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; return titleAccessor; @@ -74,7 +74,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { return this._props.axes[1] + ' vs. ' + this._props.axes[0] + ' Line Chart'; } @computed get parentViz() { - return DocCast(this._props.Document.dataViz_parentViz); + return DocCast(this._props.Doc.dataViz_parentViz); } @computed get incomingHighlited() { // return selected x and y axes @@ -113,7 +113,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { // title: 'line doc selection' + (this._currSelected?.x ?? ''), }); - PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Doc); anchor.config_dataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined; return anchor; }; diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index 86e6ad8e4..0ae70786f 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -16,7 +16,7 @@ import { PinProps, PinDocView } from '../../../PinFuncs'; import './Chart.scss'; export interface PieChartProps { - Document: Doc; + Doc: Doc; layoutDoc: Doc; axes: string[]; titleCol: string; @@ -83,7 +83,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { } @computed get parentViz() { - return DocCast(this._props.Document.dataViz_parentViz); + return DocCast(this._props.Doc.dataViz_parentViz); } componentWillUnmount() { @@ -114,7 +114,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // title: 'piechart doc selection' + this._currSelected, }); - PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Doc); return anchor; }; @@ -169,7 +169,6 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // inside the slice of it crosses an odd number of edges const showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index]; let key = 'data'; // key that represents slice - // eslint-disable-next-line prefer-destructuring if (Object.keys(showSelected)[0] === 'frequency') key = Object.keys(showSelected)[1]; if (changeSelectedVariables) { let sameAsAny = false; @@ -296,7 +295,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { if (descriptionField) dataPointVal[descriptionField] = each[descriptionField]; try { dataPointVal[percentField] = Number(dataPointVal[percentField].replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '')); - } catch (error) { + } catch { /* empty */ } possibleDataPointVals.push(dataPointVal); @@ -306,7 +305,6 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // to make sure all important slice information is on 'd' object let addKey: any = false; if (pieDataSet.length && Object.keys(pieDataSet[0])[0] === 'frequency') { - // eslint-disable-next-line prefer-destructuring addKey = Object.keys(pieDataSet[0])[1]; } arcs.append('path') @@ -324,7 +322,6 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { const sliceTitle = dataPoint[this._props.axes[0]]; const accessByName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle; sliceColors.forEach(each => { - // eslint-disable-next-line prefer-destructuring each[0] === accessByName && (sliceColor = each[1]); }); } @@ -337,7 +334,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { }); return selectThisData ? 'slice hover' : 'slice'; }) - // @ts-ignore + // @ts-expect-error types don't match .attr('d', arc) .on('click', onPointClick) .on('mouseover', onHover) @@ -388,7 +385,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { }; render() { - let titleAccessor: any = 'dataViz_pie_title'; + let titleAccessor = 'dataViz_pie_title'; if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; @@ -420,7 +417,6 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { let selectedSliceColor; const sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); sliceColors.forEach(each => { - // eslint-disable-next-line prefer-destructuring if (each[0] === curSelectedSliceName!) selectedSliceColor = each[1]; }); diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 7ef4bca6b..b73123691 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -5,7 +5,6 @@ import * as React from 'react'; import { ClientUtils, setupMoveUpEvents } from '../../../../../ClientUtils'; import { emptyFunction } from '../../../../../Utils'; import { Doc, Field, NumListCast } from '../../../../../fields/Doc'; -import { DocData } from '../../../../../fields/DocSymbols'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast } from '../../../../../fields/Types'; @@ -20,7 +19,7 @@ import './Chart.scss'; const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore interface TableBoxProps { - Document: Doc; + Doc: Doc; layoutDoc: Doc; records: { [key: string]: unknown }[]; selectAxes: (axes: string[]) => void; @@ -82,7 +81,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { } @computed get parentViz() { - return DocCast(this._props.Document.dataViz_parentViz); + return DocCast(this._props.Doc.dataViz_parentViz); } @computed get columns() { @@ -140,28 +139,28 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { e, moveEv => { // dragging off a column to create a brushed DataVizBox - const sourceAnchorCreator = () => this._props.docView?.()?.Document || this._props.Document; + const sourceAnchorCreator = () => this._props.docView?.()?.Document || this._props.Doc; const targetCreator = (annotationOn: Doc | undefined) => { const doc = this._props.docView?.()?.Document; if (doc) { const embedding = Doc.MakeEmbedding(doc); embedding._dataViz = DataVizView.TABLE; embedding._dataViz_axes = new List<string>([col]); - embedding._dataViz_parentViz = this._props.Document; + embedding._dataViz_parentViz = this._props.Doc; embedding.annotationOn = annotationOn; embedding.histogramBarColors = Field.Copy(this._props.layoutDoc.histogramBarColors); embedding.defaultHistogramColor = this._props.layoutDoc.defaultHistogramColor; embedding.pieSliceColors = Field.Copy(this._props.layoutDoc.pieSliceColors); return embedding; } - return this._props.Document; + return this._props.Doc; }; if (this._props.docView?.() && !ClientUtils.isClick(moveEv.clientX, moveEv.clientY, downX, downY, Date.now())) { DragManager.StartAnchorAnnoDrag(moveEv.target instanceof HTMLElement ? [moveEv.target] : [], new DragManager.AnchorAnnoDragData(this._props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, { dragComplete: completeEv => { if (!completeEv.aborted && completeEv.annoDragData && completeEv.annoDragData.linkSourceDoc && completeEv.annoDragData.dropDocument && completeEv.linkDocument) { - completeEv.linkDocument[DocData].link_matchEmbeddings = true; - completeEv.linkDocument[DocData].stroke_startMarker = true; + completeEv.linkDocument.$link_matchEmbeddings = true; + completeEv.linkDocument.$stroke_startMarker = true; this._props.docView?.()?._props.addDocument?.(completeEv.linkDocument); } }, diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx index a49c69be3..3b666bad5 100644 --- a/src/client/views/nodes/DiagramBox.tsx +++ b/src/client/views/nodes/DiagramBox.tsx @@ -3,7 +3,6 @@ import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; -import { DocData } from '../../../fields/DocSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { Cast, DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; import { Gestures } from '../../../pen-gestures/GestureTypes'; @@ -46,7 +45,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @observable _errorMessage = ''; @computed get mermaidcode() { - return StrCast(this.Document[DocData].text, RTFCast(this.Document[DocData].text)?.Text); + return StrCast(this.Document.$text, RTFCast(this.Document.$text)?.Text); } componentDidMount() { @@ -89,7 +88,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; setMermaidCode = undoable((res: string) => { - this.Document[DocData].text = new RichTextField( + this.Document.$text = new RichTextField( JSON.stringify({ doc: { type: 'doc', diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 47c5734f7..504c1491e 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -11,10 +11,85 @@ import { Cast, DocCast, StrCast } from '../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { ObservableReactComponent, ObserverJsxParser } from '../ObservableReactComponent'; import './DocumentView.scss'; -import { FieldViewProps } from './FieldView'; +import { FieldViewProps, FieldViewSharedProps } from './FieldView'; +import { DragManager } from '../../util/DragManager'; +import { dropActionType } from '../../util/DropActionTypes'; +import { Property } from 'csstype'; + +interface DocOnlyProps { + LayoutTemplate?: () => Opt<Doc>; + LayoutTemplateString?: string; + hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected + hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected + hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + hideDocumentButtonBar?: boolean; + hideOpenButton?: boolean; + hideDeleteButton?: boolean; + hideLinkAnchors?: boolean; + hideLinkButton?: boolean; + hideCaptions?: boolean; + contentPointerEvents?: Property.PointerEvents | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents + dontCenter?: 'x' | 'y' | 'xy'; + showTags?: boolean; + showAIEditor?: boolean; + hideFilterStatus?: boolean; + childHideDecorationTitle?: boolean; + childHideResizeHandles?: boolean; + childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. + dragWhenActive?: boolean; + dontHideOnDrag?: boolean; + onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected + DataTransition?: () => string | undefined; + NativeWidth?: () => number; + NativeHeight?: () => number; + contextMenuItems?: () => { script?: ScriptField; method?: () => void; filter?: ScriptField; label: string; icon: string }[]; + dragConfig?: (data: DragManager.DocumentDragData) => void; + dragStarting?: () => void; + dragEnding?: () => void; + + reactParent?: React.Component; // parent React component view (see CollectionFreeFormDocumentView) +} +const DocOnlyProps = [ + 'layoutFieldKey', + 'LayoutTemplate', + 'LayoutTemplateString', + 'hideDecorations', // whether to suppress all DocumentDecorations when doc is selected + 'hideResizeHandles', // whether to suppress resized handles on doc decorations when this document is selected + 'hideTitle', // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + 'hideDecorationTitle', // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + 'hideDocumentButtonBar', + 'hideOpenButton', + 'hideDeleteButton', + 'hideLinkAnchors', + 'hideLinkButton', + 'hideCaptions', + 'contentPointerEvents', // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents + 'dontCenter', + 'showTags', + 'showAIEditor', + 'hideFilterStatus', + 'childHideDecorationTitle', + 'childHideResizeHandles', + 'childDragAction', // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. + 'dragWhenActive', + 'dontHideOnDrag', + 'onClickScriptDisable', // undefined = only when selected + 'DataTransition', + 'NativeWidth', + 'NativeHeight', + 'contextMenuItems', + 'dragConfig', + 'dragStarting', + 'dragEnding', + + 'reactParent', // parent React component view (see CollectionFreeFormDocumentView) +]; + +export interface DocumentViewProps extends DocOnlyProps, FieldViewSharedProps {} type BindingProps = Without<FieldViewProps, 'fieldKey'>; -export interface JsxBindings { +interface JsxBindings { props: BindingProps; } @@ -77,7 +152,7 @@ export class HTMLtag extends React.Component<HTMLtagProps> { } } -export interface DocumentContentsViewProps extends FieldViewProps { +interface DocumentContentsViewProps extends DocumentViewProps, FieldViewProps { layoutFieldKey: string; } @observer @@ -113,29 +188,11 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte this._props.LayoutTemplate?.() || (this._props.LayoutTemplateString && this._props.Document) || (this._props.layoutFieldKey && StrCast(this._props.Document[this._props.layoutFieldKey]) && this._props.Document) || - Doc.Layout(this._props.Document, this._props.layoutFieldKey ? Cast(this._props.Document[this._props.layoutFieldKey], Doc, null) : undefined); - return Doc.expandTemplateLayout(template, this._props.Document); + Doc.LayoutDoc(this._props.Document, DocCast(this._props.Document[this._props.layoutFieldKey])); + return Doc.expandTemplateLayout(template, this._props.Document, this._props.layoutFieldKey); } CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings { - const docOnlyProps = [ - // these are the properties in DocumentViewProps that need to be removed to pass on only DocumentSharedViewProps to the FieldViews - 'hideResizeHandles', - 'hideTitle', - 'bringToFront', - 'childContentPointerEvents', - 'LayoutTemplateString', - 'LayoutTemplate', - 'showTags', - 'layoutFieldKey', - 'dontCenter', - 'DataTransition', - 'contextMenuItems', - // 'onClick', // don't need to omit this since it will be set - 'onDoubleClickScript', - 'onPointerDownScript', - 'onPointerUpScript', - ]; const templateDataDoc = this._props.TemplateDataDocument ?? (this.layoutDoc !== this._props.Document ? this._props.Document[DocData] : undefined); const list: BindingProps & React.DetailedHTMLProps<React.HtmlHTMLAttributes<HTMLDivElement>, HTMLDivElement> = { ...this._props, @@ -146,7 +203,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte }; return { props: { - ...OmitKeys(list, [...docOnlyProps], '').omit, + ...OmitKeys(list, DocOnlyProps, '').omit, } as BindingProps, }; } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index dd5fd0d0c..5ac66f2cd 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -273,7 +273,15 @@ .documentView-noAIWidgets { transform-origin: top left; - position: relative; + position: absolute; + bottom: 0; + pointer-events: none; +} +.documentView-widgetDecorations { + transform-origin: top right; + position: absolute; + top: 0; + right: 0; } .documentView-editorView-history { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index cac276535..284014e54 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -9,7 +9,7 @@ import { Fade, JackInTheBox } from 'react-awesome-reveal'; import { ClientUtils, DivWidth, isTargetChildOf as isParentOf, lightOrDark, returnFalse, returnVal, simMouseEvent, simulateMouseClick } from '../../../ClientUtils'; import { Utils, emptyFunction } from '../../../Utils'; import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc'; -import { AclAdmin, AclEdit, AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols'; +import { AclAdmin, AclEdit, AclPrivate, Animation, AudioPlay, DocViews } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -40,53 +40,22 @@ import { FieldsDropdown } from '../FieldsDropdown'; import { ObserverJsxParser } from '../ObservableReactComponent'; import { PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; +import { TagsView } from '../TagsView'; import { ViewBoxInterface } from '../ViewBoxInterface'; import { GroupActive } from './CollectionFreeFormDocumentView'; -import { DocumentContentsView } from './DocumentContentsView'; +import { DocumentContentsView, DocumentViewProps } from './DocumentContentsView'; import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; -import { FieldViewProps, FieldViewSharedProps } from './FieldView'; +import { FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import { OpenWhere, OpenWhereMod } from './OpenWhere'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails/PresEnums'; import SpringAnimation from './trails/SlideEffect'; import { SpringType, springMappings } from './trails/SpringUtils'; -import { TagsView } from '../TagsView'; -export interface DocumentViewProps extends FieldViewSharedProps { - hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected - hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected - hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings - hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings - hideDocumentButtonBar?: boolean; - hideOpenButton?: boolean; - hideDeleteButton?: boolean; - hideLinkAnchors?: boolean; - hideLinkButton?: boolean; - hideCaptions?: boolean; - contentPointerEvents?: Property.PointerEvents | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents - dontCenter?: 'x' | 'y' | 'xy'; - showTags?: boolean; - hideFilterStatus?: boolean; - childHideDecorationTitle?: boolean; - childHideResizeHandles?: boolean; - childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. - dragWhenActive?: boolean; - dontHideOnDrag?: boolean; - onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected - DataTransition?: () => string | undefined; - NativeWidth?: () => number; - NativeHeight?: () => number; - contextMenuItems?: () => { script?: ScriptField; method?: () => void; filter?: ScriptField; label: string; icon: string }[]; - dragConfig?: (data: DragManager.DocumentDragData) => void; - dragStarting?: () => void; - dragEnding?: () => void; - - reactParent?: React.Component; // parent React component view (see CollectionFreeFormDocumentView) -} @observer -export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps & { showAIEditor: boolean }>() { +export class DocumentViewInternal extends DocComponent<DocumentViewProps & FieldViewProps>() { // this makes mobx trace() statements more descriptive public get displayName() { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. @@ -134,14 +103,14 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @computed get border() { return this.style(this.layoutDoc, StyleProp.Border) as string || ""; } // prettier-ignore @computed get borderRounding() { return this.style(this.layoutDoc, StyleProp.BorderRounding) as string; } // prettier-ignore @computed get widgetDecorations() { return this.style(this.layoutDoc, StyleProp.Decorations) as JSX.Element; } // prettier-ignore - @computed get backgroundBoxColor(){ return this.style(this.Document, StyleProp.BackgroundColor + ':docView') as string; } // prettier-ignore @computed get showTitle() { return this.style(this.layoutDoc, StyleProp.ShowTitle) as Opt<string>; } // prettier-ignore @computed get showCaption() { return this.style(this.layoutDoc, StyleProp.ShowCaption) as string ?? ""; } // prettier-ignore @computed get headerMargin() { return this.style(this.layoutDoc, StyleProp.HeaderMargin) as number ?? 0; } // prettier-ignore @computed get titleHeight() { return this.style(this.layoutDoc, StyleProp.TitleHeight) as number ?? 0; } // prettier-ignore - @computed get docContents() { return this.style(this.Document, StyleProp.DocContents) as JSX.Element; } // prettier-ignore - @computed get highlighting() { return this.style(this.Document, StyleProp.Highlighting); } // prettier-ignore - @computed get borderPath() { return this.style(this.Document, StyleProp.BorderPath); } // prettier-ignore + @computed get backgroundBoxColor(){ return this.style(this.Document, StyleProp.BackgroundColor + ':docView') as string; } // prettier-ignore + @computed get docContents() { return this.style(this.Document, StyleProp.DocContents) as JSX.Element; } // prettier-ignore + @computed get highlighting() { return this.style(this.Document, StyleProp.Highlighting); } // prettier-ignore + @computed get borderPath() { return this.style(this.Document, StyleProp.BorderPath); } // prettier-ignore @computed get onClickHdlr() { return this._props.onClickScript?.() ?? ScriptCast(this.layoutDoc.onClick ?? this.Document.onClick); } // prettier-ignore @computed get onDoubleClickHdlr() { return this._props.onDoubleClickScript?.() ?? ScriptCast(this.layoutDoc.onDoubleClick ?? this.Document.onDoubleClick); } // prettier-ignore @@ -200,9 +169,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } componentDidMount() { - runInAction(() => { - this._mounted = true; - }); + runInAction(() => (this._mounted = true)); this.setupHandlers(); this._disposers.contentActive = reaction( () => @@ -214,16 +181,12 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document : Doc.ActiveTool !== InkTool.None || SnappingManager.CanEmbed || this.rootSelected() || this.Document.forceActive || this._componentView?.isAnyChildContentActive?.() || this._props.isContentActive() ? true : undefined, - active => { - this._isContentActive = active; - }, + active => (this._isContentActive = active), { fireImmediately: true } ); this._disposers.pointerevents = reaction( () => this.style(this.Document, StyleProp.PointerEvents) as Property.PointerEvents | undefined, - pointerevents => { - this._pointerEvents = pointerevents; - }, + pointerevents => (this._pointerEvents = pointerevents), { fireImmediately: true } ); } @@ -336,7 +299,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }; const clickFunc = this.onClickFunc?.()?.script ? () => (this.onClickFunc?.()?.script.run(scriptProps, console.log).result as Opt<{ select: boolean }>)?.select && this._props.select(false) : undefined; if (!clickFunc) { - // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part + // onDragStart implies a button doc that we don't want to select when clicking. if (this.layoutDoc.onDragStart && !(e.ctrlKey || e.button > 0)) stopPropagate = false; preventDefault = false; } @@ -426,7 +389,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document noOnClick = undoable(() => { this.Document.ignoreClick = false; - this.Document.onClick = this.Document[DocData].onClick = undefined; + this.Document.onClick = this.Document.$onClick = undefined; }, 'default on click'); deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc'); @@ -533,7 +496,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const items = this._props.styleProvider?.(this.Document, this._props, StyleProp.ContextMenuItems) as ContextMenuProps[]; items?.forEach(item => ContextMenu.Instance.addItem(item)); - const customScripts = Cast(this.Document.contextMenuScripts, listSpec(ScriptField), []); + const customScripts = Cast(this.Document.contextMenuScripts, listSpec(ScriptField), [])!; StrListCast(this.Document.contextMenuLabels).forEach((label, i) => cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' }) ); @@ -554,7 +517,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' }); } appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'map-pin' }); - appearanceItems.push({ description: 'AI view', event: () => this._docView?.toggleAIEditor(), icon: 'map-pin' }); + this._componentView?.componentAIView?.() && appearanceItems.push({ description: 'AI view', event: () => this._docView?.toggleAIEditor(), icon: 'map-pin' }); !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' }); @@ -567,9 +530,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document zorderItems.push({ description: !this.layoutDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged', event: undoable( - action(() => { - this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged; - }), + action(() => (this.layoutDoc._keepZWhenDragged = !this.layoutDoc._keepZWhenDragged)), 'set zIndex drag' ), icon: 'hand-point-up', @@ -691,10 +652,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document this._props .ScreenToLocalTransform() .translate(-NumCast(this.Document.borderWidth), -this.headerMargin - NumCast(this.Document.borderWidth)) - .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.aiContentsHeight() : 1); - onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr; + .scale(this.viewingAiEditor() ? (this._props.PanelHeight() || 1) / this.aiContentsHeight() : 1); + onClickFunc = () => (this.disableClickScriptFunc ? undefined : this.onClickHdlr); setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height + 2 * NumCast(this.Document.borderWidth))); } // prettier-ignore - setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore + setContentView = action((view: ViewBoxInterface<FieldViewProps>) => (this._componentView = view)); isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; @@ -719,12 +680,12 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @observable _aiWinHeight = 88; - private _tagsBtnHeight = 22; + TagsBtnHeight = 22; @computed get currentScale() { const viewXfScale = this._props.DocumentView!().screenToLocalScale(); - const x = NumCast(this.Document.height) / viewXfScale / 80; + const x = NumCast(this.Document._height) / viewXfScale / 80; const xscale = x >= 1 ? 0 : 1 / (1 + x * (viewXfScale - 1)); - const y = NumCast(this.Document.width) / viewXfScale / 200; + const y = NumCast(this.Document._width) / viewXfScale / 200; const yscale = y >= 1 ? 0 : 1 / (1 + y * viewXfScale - 1); return Math.max(xscale, yscale, 1 / viewXfScale); } @@ -733,16 +694,68 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document */ @computed get viewScaling() { return 1 / this.currentScale; } // 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. + * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its nominal pixel size. */ - @computed get maxWidgetSize() { return Math.min(this._tagsBtnHeight * this.viewScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height))); } // prettier-ignore + @computed get maxWidgetSize() { return Math.min(this.TagsBtnHeight * this.viewScaling, 0.25 * Math.min(NumCast(this.Document._width), NumCast(this.Document._height))); } // prettier-ignore /** * 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 uiBtnScaling() { return Math.max(this.maxWidgetSize / this._tagsBtnHeight, 1) * Math.min(1, this.viewScaling); } // prettier-ignore + @computed get uiBtnScaling() { return Math.max(this.maxWidgetSize / this.TagsBtnHeight, 1) * Math.min(1, this.viewScaling); } // prettier-ignore aiContentsWidth = () => (this.aiContentsHeight() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1); aiContentsHeight = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight * this.uiBtnScaling); + @computed get aiEditor() { + return ( + <> + <div + className="documentView-editorView-history" + ref={r => this.historyRef(this._oldAiWheel, (this._oldAiWheel = r))} + style={{ + transform: `scale(${this.uiBtnScaling})`, + height: this.aiContentsHeight() / this.uiBtnScaling, + width: ((this._props.PanelWidth() - this.aiContentsWidth()) * 0.95) / this.uiBtnScaling, + }}> + {this._componentView?.componentAIViewHistory?.() ?? null} + </div> + <div + className="documentView-editorView" + style={{ + background: SnappingManager.userVariantColor, + width: `${100 / this.uiBtnScaling}%`, // + transform: `scale(${this.uiBtnScaling})`, + }} + ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}> + <div className="documentView-editorView-resizer" /> + {this._componentView?.componentAIView?.() ?? null} + {this._props.DocumentView?.() ? <TagsView background={this.backgroundBoxColor} Views={[this._props.DocumentView?.()]} /> : null} + </div> + </> + ); + } + @computed get tagsOverlay() { + return ( + <div + className="documentView-noAiWidgets" + style={{ + width: `${100 / this.uiBtnScaling}%`, // + transform: `scale(${this.uiBtnScaling})`, + height: Number.isNaN(this.maxWidgetSize) ? undefined : this.TagsBtnHeight * this.uiBtnScaling, + }}> + {this._props.DocumentView?.() && !this._props.docViewPath().slice(-2)[0].ComponentView?.isUnstyledView?.() ? <TagsView background={this.backgroundBoxColor} Views={[this._props.DocumentView?.()]} /> : null} + </div> + ); + } + tagsOverlayFunc = () => (this._props.DocumentView?.().showTags ? this.tagsOverlay : null); + @computed get widgetOverlay() { + return ( + <div className="documentView-widgetDecorations" style={{ transform: `scale(${this.uiBtnScaling})` }}> + {this.widgetDecorations} + </div> + ); + } + widgetOverlayFunc = () => (this.widgetDecorations ? this.widgetOverlay : null); + viewingAiEditor = () => (this._props.showAIEditor && this._componentView?.componentAIView?.() !== undefined ? this.aiEditor : null); + _contentsRef = React.createRef<DocumentContentsView>(); @computed get viewBoxContents() { TraceMobx(); const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString; @@ -753,17 +766,18 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document className="documentView-contentsView" style={{ pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'), - width: this._props.showAIEditor ? this.aiContentsWidth() : undefined, - height: this._props.showAIEditor ? this.aiContentsHeight() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, + width: this.viewingAiEditor() ? this.aiContentsWidth() : undefined, + height: this.viewingAiEditor() ? this.aiContentsHeight() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> <DocumentContentsView {...this._props} + ref={this._contentsRef} layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')} pointerEvents={this.contentPointerEvents} setContentViewBox={this.setContentView} childFilters={this.childFilters} - PanelWidth={this._props.showAIEditor ? this.aiContentsWidth : this._props.PanelWidth} - PanelHeight={this._props.showAIEditor ? this.aiContentsHeight : this.panelHeight} + PanelWidth={this.viewingAiEditor() ? this.aiContentsWidth : this._props.PanelWidth} + PanelHeight={this.viewingAiEditor() ? this.aiContentsHeight : this.panelHeight} setHeight={this.setHeight} isContentActive={this.isContentActive} ScreenToLocalTransform={this.screenToLocalContent} @@ -773,43 +787,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)} /> </div> - {!this._props.showAIEditor ? ( - <div - className="documentView-noAiWidgets" - style={{ - width: `${100 / this.uiBtnScaling}%`, // - transform: `scale(${this.uiBtnScaling})`, - bottom: Number.isNaN(this.maxWidgetSize) ? undefined : this.maxWidgetSize, - }}> - {this._props.DocumentView?.() && !this._props.docViewPath().slice(-2)[0].ComponentView?.isUnstyledView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null} - </div> - ) : ( - <> - <div - className="documentView-editorView-history" - ref={r => this.historyRef(this._oldAiWheel, (this._oldAiWheel = r))} - style={{ - transform: `scale(${this.uiBtnScaling})`, - height: this.aiContentsHeight() / this.uiBtnScaling, - width: ((this._props.PanelWidth() - this.aiContentsWidth()) * 0.95) / this.uiBtnScaling, - }}> - {this._componentView?.componentAIViewHistory?.() ?? null} - </div> - <div - className="documentView-editorView" - style={{ - background: SnappingManager.userVariantColor, - width: `${100 / this.uiBtnScaling}%`, // - transform: `scale(${this.uiBtnScaling})`, - }} - ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}> - <div className="documentView-editorView-resizer" /> - {this._componentView?.componentAIView?.() ?? null} - {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null} - </div> - </> - )} - {this.widgetDecorations ?? null} + {this.viewingAiEditor() ?? this.tagsOverlayFunc()} + {this.widgetOverlayFunc()} </> ); } @@ -827,11 +806,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); fieldsDropdown = (placeholder: string) => ( <div - ref={r => { r && runInAction(() => (this._titleDropDownInnerWidth = DivWidth(r)));}} // prettier-ignore - onPointerDown={action(() => { this._changingTitleField = true; })} // prettier-ignore + ref={action((r:HTMLDivElement|null) => r && (this._titleDropDownInnerWidth = DivWidth(r)))} // prettier-ignore + onPointerDown={action(() => (this._changingTitleField = true))} style={{ width: 'max-content', background: SnappingManager.userBackgroundColor, color: SnappingManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> <FieldsDropdown - Document={this.Document} + Doc={this.Document} placeholder={placeholder} selectFunc={action((field: string | number) => { if (this.layoutDoc.layout_showTitle) { @@ -841,7 +820,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } this._changingTitleField = false; })} - menuClose={action(() => { this._changingTitleField = false; })} // prettier-ignore + menuClose={action(() => (this._changingTitleField = false))} /> </div> ); @@ -857,7 +836,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const background = StrCast( this.layoutDoc.layout_headingColor, // StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, - StrCast(Doc.SharingDoc().headingColor, SnappingManager.userBackgroundColor) + StrCast(Doc.SharingDoc()?.headingColor, SnappingManager.userBackgroundColor) // ) ); const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._titleDropDownInnerWidth * this.titleHeight) / 30) : 0; @@ -1043,7 +1022,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document root: Doc ) { const effectDirection = (presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) as PresEffectDirection; - const duration = Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)); + const duration = Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null) ?? null); const effectProps = { left: effectDirection === PresEffectDirection.Left, right: effectDirection === PresEffectDirection.Right, @@ -1158,7 +1137,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public static GetDocImage(doc?: Doc) { return DocumentView.getDocumentView(doc) ?.ComponentView?.updateIcon?.() - .then(() => ImageCast(doc!.icon, ImageCast(doc![Doc.LayoutFieldKey(doc!)]))); + .then(() => ImageCast(doc!.icon, ImageCast(doc![Doc.LayoutDataKey(doc!)]))); } public get displayName() { return 'DocumentView(' + (this.Document?.title??"") + ')'; } // prettier-ignore @@ -1176,7 +1155,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public static UniquifyId(inLightbox: boolean | undefined, id: string) { return (inLightbox ? 'lightbox-' : '') + id; } - public ViewGuid = DocumentView.UniquifyId(DocumentView.LightboxContains(this), Utils.GenerateGuid()); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations. + public ViewGuid = DocumentView.UniquifyId(DocumentView.LightboxContains(this), 'D' + Utils.GenerateGuid()); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations. public DocUniqueId = DocumentView.UniquifyId(DocumentView.LightboxContains(this), this.Document[Id]); constructor(props: DocumentViewProps) { @@ -1194,6 +1173,25 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { @observable private _selected = false; @observable public static CurrentlyPlaying: DocumentView[] = []; // audio or video media views that are currently playing @observable public TagPanelHeight = 0; + @observable public TagPanelEditing = false; + + /** + * Tests whether the component Doc being rendered matches the Doc that this view thinks its rendering. + * When switching the layout_fieldKey, component views may react before the internal DocumentView has re-rendered. + * If this happens, the component view may try to write invalid data, such as when expanding a template (which + * depends on the DocumentView being in synch with the subcomponents). + * @param renderDoc sub-component Doc being rendered + * @returns boolean whether sub-component Doc is in synch with the layoutDoc that this view thinks its rendering + */ + IsInvalid = (renderDoc?: Doc): boolean => { + const docContents = this._docViewInternal?._contentsRef.current; + return !( + (!renderDoc || + (docContents?.layoutDoc === renderDoc && // + !this.docViewPath().some(dv => dv.IsInvalid()))) && + docContents?._props.layoutFieldKey === this.Document.layout_fieldKey + ); + }; @computed get showTags() { return this.Document._layout_showTags || this._props.showTags; @@ -1210,7 +1208,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } @computed private get nativeScaling() { if (this.shouldNotScale) return 1; - const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; + const minTextScale = [DocumentType.RTF, DocumentType.JOURNAL].includes(this.Document.type as DocumentType) ? 0.1 : 0; const ai = this._showAIEditor && this.nativeWidth === this.layoutDoc.width ? 95 : 0; const effNW = Math.max(this.effectiveNativeWidth - ai, 1); const effNH = Math.max(this.effectiveNativeHeight - ai, 1); @@ -1264,16 +1262,19 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentView.removeView(this); } - public set IsSelected(val) { runInAction(() => { this._selected = val; }); } // prettier-ignore + public set IsSelected(val) { runInAction(() => (this._selected = val)) } // prettier-ignore public get IsSelected() { return this._selected; } // prettier-ignore public get IsContentActive(){ return this._docViewInternal?.isContentActive(); } // prettier-ignore public get topMost() { return this._props.renderDepth === 0; } // prettier-ignore public get ContentDiv() { return this._docViewInternal?._contentDiv; } // prettier-ignore public get ComponentView() { return this._docViewInternal?._componentView; } // prettier-ignore public get allLinks() { return this._docViewInternal?._allLinks ?? []; } // prettier-ignore + public get TagBtnHeight() { return this._docViewInternal?.TagsBtnHeight; } // prettier-ignore + public get UIBtnScaling() { return this._docViewInternal?.uiBtnScaling; } // prettier-ignore + public get HasAIEditor() { return !!this._docViewInternal?._componentView?.componentAIView?.(); } // prettier-ignore get LayoutFieldKey() { - return Doc.LayoutFieldKey(this.Document, this._props.LayoutTemplateString); + return Doc.LayoutDataKey(this.Document, this._props.LayoutTemplateString); } @computed get layout_fitWidth() { @@ -1327,10 +1328,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public toggleNativeDimensions = () => this._docViewInternal && this.Document.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.NativeDimScaling() ?? 1, this._props.PanelWidth(), this._props.PanelHeight()); - public iconify(finished?: () => void, animateTime?: number) { + public iconify = action((finished?: () => void, animateTime?: number) => { this.ComponentView?.updateIcon?.(); const animTime = this._docViewInternal?.animateScaleTime(); - runInAction(() => { this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime); }); // prettier-ignore + this._docViewInternal && animateTime !== undefined && (this._docViewInternal._animateScaleTime = animateTime); const finalFinished = action(() => { finished?.(); this._docViewInternal && (this._docViewInternal._animateScaleTime = animTime); @@ -1340,15 +1341,15 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { this.switchViews(true, 'icon', finalFinished); if (layoutFieldKey && layoutFieldKey !== 'layout' && layoutFieldKey !== 'layout_icon') this.Document.deiconifyLayout = layoutFieldKey.replace('layout_', ''); } else { - const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); + const deiconifyLayout = StrCast(this.Document.deiconifyLayout); this.switchViews(!!deiconifyLayout, deiconifyLayout, finalFinished, true); this.Document.deiconifyLayout = undefined; this._props.bringToFront?.(this.Document); } - } + }); public playAnnotation = () => { - const audioAnnoState = this.dataDoc.audioAnnoState ?? AudioAnnoState.stopped; + const audioAnnoState = this.Document._audioAnnoState ?? AudioAnnoState.stopped; const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null); const anno = audioAnnos?.lastElement(); if (anno instanceof AudioField) { @@ -1360,13 +1361,13 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { autoplay: true, loop: false, volume: 0.5, - onend: action(() => { this.dataDoc.audioAnnoState = AudioAnnoState.stopped; }), // prettier-ignore + onend: action(() => (this.Document._audioAnnoState = AudioAnnoState.stopped)), }); - this.dataDoc.audioAnnoState = AudioAnnoState.playing; + this.Document._audioAnnoState = AudioAnnoState.playing; break; case AudioAnnoState.playing: (this.dataDoc[AudioPlay] as Howl)?.stop(); - this.dataDoc.audioAnnoState = AudioAnnoState.stopped; + this.Document._audioAnnoState = AudioAnnoState.stopped; break; default: } @@ -1416,14 +1417,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { tempDoc = view.Document; MakeTemplate(tempDoc); Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc); - Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc)); - tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc); + Doc.AddDocToList(DocListCast(Doc.MyTools?.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc)); + DocCast(Doc.UserDoc().template_user) && tempDoc && Doc.AddDocToList(DocCast(Doc.UserDoc().template_user)!, 'data', tempDoc); } else { - tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]); - if (!tempDoc) { - tempDoc = view.Document; - while (tempDoc && !Doc.isTemplateDoc(tempDoc)) tempDoc = DocCast(tempDoc.proto); - } + tempDoc = DocCast(Doc.LayoutField(view.Document)); } } Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined; @@ -1445,10 +1442,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(!!defaultLayout, defaultLayout, undefined, true); else this.switchViews(true, detailLayoutKeySuffix, undefined, true); }; - public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { + public switchViews = action((custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { const batch = UndoManager.StartBatch('switchView:' + view); // shrink doc first.. - runInAction(() => { this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1); }); // prettier-ignore + this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1); setTimeout( action(() => { if (useExistingLayout && custom && this.Document['layout_' + view]) { @@ -1468,7 +1465,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { }), Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10) ); - }; + }); /** * @returns a hierarchy path through the nested DocumentViews that display this view. The last element of the path is this view. */ @@ -1521,7 +1518,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { ref={r => { const val = r?.style.display !== 'none'; // if the outer overlay has been displayed, trigger the innner div to start it's opacity fade in transition if (r && val !== this._enableHtmlOverlayTransitions) { - setTimeout(action(() => { this._enableHtmlOverlayTransitions = val; })); // prettier-ignore + setTimeout(action(() => (this._enableHtmlOverlayTransitions = val))); } }} style={{ display: !this._htmlOverlayText ? 'none' : undefined }}> @@ -1548,12 +1545,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { <div id={this.ViewGuid} className="contentFittingDocumentView" - onPointerEnter={action(() => { - this._isHovering = true; - })} - onPointerLeave={action(() => { - this._isHovering = false; - })}> + onPointerEnter={action(() => (this._isHovering = true))} // + onPointerLeave={action(() => (this._isHovering = false))}> {!this.Document || !this._props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" @@ -1581,9 +1574,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { fitWidth={this.layout_fitWidthFunc} ScreenToLocalTransform={this.screenToContentsTransform} focus={this._props.focus || emptyFunction} - ref={action((r: DocumentViewInternal | null) => { - r && (this._docViewInternal = r); - })} + ref={action((r: DocumentViewInternal | null) => r && (this._docViewInternal = r))} /> {this.htmlOverlay()} {this.ComponentView?.infoUI?.()} @@ -1627,7 +1618,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) { DocumentView.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document)); } else { - const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc)); + const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc))!; const showDoc = !Doc.IsSystem(container) && !cv ? container : doc; options.toggleTarget = undefined; DocumentView.showDocument(showDoc, options, () => DocumentView.showDocument(doc, { ...options, openLocation: undefined })).then(() => { @@ -1706,7 +1697,7 @@ ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuff // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) { - const collectedLinks = DocListCast(linkCollection[DocData].data); + const collectedLinks = DocListCast(linkCollection.$data); let wid = NumCast(linkSource._width); let embedding: Doc | undefined; const links = Doc.Links(linkSource); @@ -1729,7 +1720,7 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSour ScriptingGlobals.add(function updateTagsCollection(collection: Doc) { const tag = StrCast(collection.title).split('-->')[1]; const matchedTags = Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, tag, false, ['tags']).keys()); - const collectionDocs = DocListCast(collection[DocData].data).concat(collection); + const collectionDocs = DocListCast(collection.$data).concat(collection); let wid = 100; let created = false; const matchedDocs = matchedTags @@ -1749,6 +1740,6 @@ ScriptingGlobals.add(function updateTagsCollection(collection: Doc) { return aset; }, new Set<Doc>()); - created && (collection[DocData].data = new List<Doc>(Array.from(matchedDocs))); + created && (collection.$data = new List<Doc>(Array.from(matchedDocs))); return true; }); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 2e40f39ed..de49f502f 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -15,6 +15,7 @@ import { FocusViewOptions } from './FocusViewOptions'; import { OpenWhere } from './OpenWhere'; import { WebField } from '../../../fields/URLField'; import { ContextMenuProps } from '../ContextMenuItem'; +import { FormattedTextBox } from './formattedText/FormattedTextBox'; export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>; export type StyleProviderFuncType = ( @@ -47,11 +48,9 @@ export type StyleProviderFuncType = ( export interface FieldViewSharedProps { Document: Doc; TemplateDataDocument?: Doc; - LayoutTemplateString?: string; - 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) + screenXPadding?: (view: DocumentView | undefined) => number; // padding in screen space coordinates (used by text box to reflow around UI buttons in carouselView) xPadding?: number; yPadding?: number; dontRegisterView?: boolean; @@ -61,7 +60,8 @@ export interface FieldViewSharedProps { ignoreAutoHeight?: boolean; disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. hideClickBehaviors?: boolean; // whether to suppress menu item options for changing click behaviors - ignoreUsePath?: boolean; // ignore the usePath field for selecting the fieldKey (eg., on text docs) + suppressSetHeight?: boolean; + dontCenter?: 'x' | 'y' | 'xy' | undefined; LocalRotation?: () => number | undefined; // amount of rotation applied to freeformdocumentview containing document view containerViewPath?: () => DocumentView[]; fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document @@ -79,14 +79,12 @@ export interface FieldViewSharedProps { styleProvider: Opt<StyleProviderFuncType>; setTitleFocus?: () => void; focus: FocusFuncType; - onClickScript?: () => ScriptField; - onDoubleClickScript?: () => ScriptField; + onClickScript?: () => ScriptField | undefined; + onDoubleClickScript?: () => ScriptField | undefined; onPointerDownScript?: () => ScriptField; onPointerUpScript?: () => ScriptField; - // eslint-disable-next-line no-use-before-define - onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined; + onKey?: (e: KeyboardEvent, textBox: FormattedTextBox) => boolean | undefined; fitWidth?: (doc: Doc) => boolean | undefined; - dontCenter?: 'x' | 'y' | 'xy' | undefined; searchFilterDocs: () => Doc[]; showTitle?: () => string; whenChildContentsActiveChanged: (isActive: boolean) => void; @@ -102,7 +100,6 @@ export interface FieldViewSharedProps { waitForDoubleClickToClick?: () => 'never' | 'always' | undefined; defaultDoubleClick?: () => 'default' | 'ignore' | undefined; pointerEvents?: () => Opt<Property.PointerEvents>; - suppressSetHeight?: boolean; } /** @@ -110,13 +107,13 @@ export interface FieldViewSharedProps { * */ export interface FieldViewProps extends FieldViewSharedProps { DocumentView?: () => DocumentView; - fieldKey: string; isSelected: () => boolean; select: (ctrlPressed: boolean, shiftPress?: boolean) => void; docViewPath: () => DocumentView[]; setHeight?: (height: number) => void; NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal isHovering?: () => boolean; + fieldKey: string; // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React) // See currentUserUtils headerTemplate for examples of creating text boxes from html which set some of these fields diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index f699568f1..3190757e2 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { ClientUtils, DashColor, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; +import { ClientUtils, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; import { ScriptField } from '../../../../fields/ScriptField'; @@ -134,7 +134,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { min={NumCast(this.dataDoc.numBtnMin, 0)} max={NumCast(this.dataDoc.numBtnMax, 100)} number={checkResult} - size={Size.XSMALL} + size={Size.XXSMALL} setNumber={undoable(value => numScript(value), `${this.Document.title} button set from list`)} fillWidth /> @@ -252,7 +252,9 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { * Color button */ @computed get colorButton() { - const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; + const color = SnappingManager.userColor; + const pickedColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; + const background = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string; const curColor = (this.colorScript?.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result as string) ?? 'transparent'; const tooltip: string = StrCast(this.Document.toolTip); @@ -270,10 +272,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { }} defaultPickerType="Classic" selectedColor={curColor} - type={Type.PRIM} + type={Type.TERT} color={color} - background={SnappingManager.userBackgroundColor} - icon={this.Icon(color) ?? undefined} + background={background} + icon={this.Icon(pickedColor) ?? undefined} tooltip={tooltip} label={this.label} /> @@ -287,19 +289,20 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const toggleStatus = script?.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean; const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; + const background = this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; const items = DocListCast(this.dataDoc.data); const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType)); return ( <MultiToggle tooltip={`Click to Toggle ${tooltip} or select new option`} - type={Type.PRIM} + type={Type.TERT} color={color} - background={undefined} + background={background} multiSelect={true} onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))} - isToggle={false} toggleStatus={toggleStatus} + showUntilToggle={BoolCast(this.Document.showUntilToggle)} label={selectedItems.length === 1 ? selectedItems[0] : this.label} items={items.map(item => ({ icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />, @@ -339,11 +342,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { <Toggle tooltip={`Toggle ${tooltip}`} toggleType={ToggleType.BUTTON} - type={inkShapeHack ? Type.TERT : Type.PRIM} + type={Type.TERT} toggleStatus={toggleStatus} text={buttonText} color={color} - background={inkShapeHack ? DashColor(SnappingManager.userBackgroundColor).darken(0.05).toString() : undefined} + triState={inkShapeHack} + background={color} icon={this.Icon(color)!} label={this.label} onPointerDown={e => @@ -404,12 +408,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { case ButtonType.ColorButton: return this.colorButton; case ButtonType.MultiToggleButton: return this.multiToggleButton; case ButtonType.ToggleButton: return this.toggleButton; - case ButtonType.ClickButton:return <IconButton {...btnProps} size={Size.MEDIUM} color={color} />; - case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} />; - case ButtonType.TextButton: return <Button {...btnProps} color={color} - background={SnappingManager.userBackgroundColor} text={StrCast(this.dataDoc.buttonText)}/>; - case ButtonType.MenuButton: return <IconButton {...btnProps} color={color} - background={SnappingManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; + case ButtonType.ClickButton: return <IconButton {...btnProps} size={Size.MEDIUM} color={color} background={color} />; + case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} background={color} />; + case ButtonType.TextButton: return <Button {...btnProps} color={color} background={color} + text={StrCast(this.dataDoc.buttonText)}/>; + case ButtonType.MenuButton: return <IconButton size={Size.LARGE} {...btnProps} color={color} background={color} + tooltipPlacement='right' onClick={scriptFunc} />; default: } return this.defaultButton; diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss index 202b0c701..d6cf95958 100644 --- a/src/client/views/nodes/IconTagBox.scss +++ b/src/client/views/nodes/IconTagBox.scss @@ -4,7 +4,6 @@ display: flex; position: relative; pointer-events: none; - background-color: rgb(218, 218, 218); border-radius: 50px; align-items: center; gap: 5px; diff --git a/src/client/views/nodes/IconTagBox.tsx b/src/client/views/nodes/IconTagBox.tsx index ddabd61e1..0bbd6a0d3 100644 --- a/src/client/views/nodes/IconTagBox.tsx +++ b/src/client/views/nodes/IconTagBox.tsx @@ -1,22 +1,23 @@ -import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; -import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; -import { emptyFunction } from '../../../Utils'; -import { Doc } from '../../../fields/Doc'; +import { Doc, StrListCast } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; +import { AudioAnnoState } from '../../../server/SharedMediaTypes'; import { undoable } from '../../util/UndoManager'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { TagItem } from '../TagsView'; import { DocumentView } from './DocumentView'; import './IconTagBox.scss'; +import { Size, Toggle, ToggleType, Type } from '@dash/components'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { StyleProp } from '../StyleProp'; export interface IconTagProps { Views: DocumentView[]; - IsEditing: boolean; + IsEditing: boolean | undefined; } /** @@ -52,13 +53,46 @@ export class IconTagBox extends ObservableReactComponent<IconTagProps> { * @param key metadata icon button * @returns an icon for the metdata button */ - getButtonIcon = (doc: Doc, key: Doc): JSX.Element => { + getButtonIcon = (dv: DocumentView, key: Doc): JSX.Element => { const icon = StrCast(key.icon) as IconProp; const tag = StrCast(key.toolType); - const isActive = TagItem.docHasTag(doc, tag); - const color = isActive ? '#4476f7' : '#323232'; // TODO should use theme colors + const color = dv._props.styleProvider?.(dv.layoutDoc, dv.ComponentView?._props, StyleProp.FontColor) as string; + return ( + <Toggle + tooltip={`Click to add/remove the tag ${tag}`} + toggleStatus={TagItem.docHasTag(dv.Document, tag)} + toggleType={ToggleType.BUTTON} + icon={<FontAwesomeIcon className={`fontIconBox-icon-${ToggleType.BUTTON}`} icon={icon} color={color} />} + size={Size.XSMALL} + type={Type.PRIM} + onClick={() => this.setIconTag(tag, !TagItem.docHasTag(this.View.Document, tag))} + color={color} + /> + ); + }; - return <FontAwesomeIcon icon={icon} style={{ color, height: '20px', width: '20px' }} />; + /** + * Displays a button to play audio annotations on the document. + * NOTE: This should be generalized -- audio should + * @returns + */ + renderAudioButtons = (dv: DocumentView, anno: string) => { + const fcolor = dv._props.styleProvider?.(dv.layoutDoc, dv.ComponentView?._props, StyleProp.FontColor) as string; + const audioIconColors: { [key: string]: string } = { playing: 'green', stopped: fcolor ?? 'blue', recording: 'red' }; + const audioAnnoState = (audioDoc: Doc) => StrCast(audioDoc.audioAnnoState, AudioAnnoState.stopped); + const color = audioIconColors[audioAnnoState(this.View.Document)]; + return ( + <Toggle + tooltip={`click to play:${anno}`} + toggleStatus={true} + toggleType={ToggleType.BUTTON} + icon={<FontAwesomeIcon className={`fontIconBox-icon-${ToggleType.BUTTON}`} icon="file-audio" color={color} />} + size={Size.XSMALL} + type={Type.PRIM} + onClick={() => this.View?.playAnnotation()} + color={color} + /> + ); }; /** @@ -69,22 +103,15 @@ export class IconTagBox extends ObservableReactComponent<IconTagProps> { .map(key => ({ key, tag: StrCast(key.toolType) })) .filter(({ tag }) => this._props.IsEditing || TagItem.docHasTag(this.View.Document, tag) || (DocumentView.Selected().length === 1 && this.View.IsSelected)) .map(({ key, tag }) => ( - <Tooltip key={tag} title={<div className="dash-tooltip">Click to add/remove this card from the {tag} group</div>}> - <button - type="button" - onPointerDown={e => - setupMoveUpEvents(this, e, returnFalse, emptyFunction, clickEv => { - this.setIconTag(tag, !TagItem.docHasTag(this.View.Document, tag)); - clickEv.stopPropagation(); - }) - }> - {this.getButtonIcon(this.View.Document, key)} - </button> + <Tooltip key={tag} title={<div className="dash-tooltip">Click to add/remove the {tag} tag</div>}> + {this.getButtonIcon(this.View, key)} </Tooltip> )); // prettier-ignore - return !buttons.length ? null : ( + const audioannos = StrListCast(this.View.Document[Doc.LayoutDataKey(this.View.Document) + '_audioAnnotations_text']); + return !buttons.length && !audioannos.length ? null : ( <div className="card-button-container" style={{ fontSize: '50px' }}> + {audioannos.length ? this.renderAudioButtons(this.View, audioannos.lastElement()) : null} {buttons} </div> ); diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index d7a14a9df..9fc20ffd4 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -114,7 +114,6 @@ width: 100%; height: 100%; position: absolute; - background: black; display: flex; flex-direction: row; align-items: center; @@ -143,11 +142,8 @@ } } .imageBox-regenerateDropTarget { - right: 30; - border-radius: 50%; - svg { - border-radius: 50%; - } + right: 35; + transform-origin: 70px 35px; } .imageBox-fader img { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 033b0b5a2..c3df82611 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,4 +1,4 @@ -import { Button, Colors, Size, Type } from '@dash/components'; +import { Button, Colors, EditableText, Size, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Slider, Tooltip } from '@mui/material'; import axios from 'axios'; @@ -8,16 +8,17 @@ import { extname } from 'path'; import * as React from 'react'; import { AiOutlineSend } from 'react-icons/ai'; import ReactLoading from 'react-loading'; -import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; +import { ClientUtils, DashColor, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; +import { ComputedField } from '../../../fields/ScriptField'; import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { Upload } from '../../../server/SharedMediaTypes'; import { emptyFunction } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -45,9 +46,8 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; -import { RichTextField } from '../../../fields/RichTextField'; -import { List } from '../../../fields/List'; +const DefaultPath = '/assets/unknown-file-icon-hi.png'; export class ImageEditorData { // eslint-disable-next-line no-use-before-define private static _instance: ImageEditorData; @@ -152,11 +152,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }, { fireImmediately: true, delay: 1000 } ); - const { layoutDoc } = this; this._disposers.path = reaction( - () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }), - ({ nativeSize, width }) => { - if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !this.layoutDoc._height) { + () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }), + ({ nativeSize, width, height }) => { + if (!this.layoutDoc._layout_nativeDimEditable || !height) { this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; } }, @@ -224,19 +223,21 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { if (de.metaKey || hitDropTarget(e.target as HTMLElement, this._overlayIconRef.current)) { added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; + this.Document.$backgroundColor_alternate = ComputedField.MakeFunction('this.data_alternates[0]?.$backgroundColor'); return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop); }, true); } else if (hitDropTarget(e.target as HTMLElement, this._regenerateIconRef.current)) { this._regenerateLoading = true; - const drag = de.complete.docDragData?.draggedDocuments.lastElement(); - const dragField = drag[Doc.LayoutFieldKey(drag)]; + const drag = de.complete.docDragData.draggedDocuments.lastElement(); + const dragField = drag[Doc.LayoutDataKey(drag)]; + const descText = RTFCast(dragField)?.Text || StrCast(dragField) || RTFCast(drag.text)?.Text || StrCast(drag.text) || StrCast(this.Document.title); const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title)); const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text); - DrawingFillHandler.drawingToImage(this.Document, 100, newPrompt(dragField instanceof RichTextField ? dragField.Text : ''), drag)?.then(action(() => (this._regenerateLoading = false))); + DrawingFillHandler.drawingToImage(this.Document, 90, newPrompt(descText), drag)?.then(action(() => (this._regenerateLoading = false))); added = false; } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; - const targetField = Doc.LayoutFieldKey(layoutDoc); + const targetField = Doc.LayoutDataKey(layoutDoc); const targetDoc = layoutDoc[DocData]; if (targetDoc[targetField] instanceof ImageField) { added = true; @@ -266,17 +267,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const nw = nscale / oldnativeWidth; this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw; this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw; - this.dataDoc._freeform_panX = nw * NumCast(this.dataDoc._freeform_panX); - this.dataDoc._freeform_panY = nw * NumCast(this.dataDoc._freeform_panY); - this.dataDoc._freeform_panX_max = this.dataDoc._freeform_panX_max ? nw * NumCast(this.dataDoc._freeform_panX_max) : undefined; - this.dataDoc._freeform_panX_min = this.dataDoc._freeform_panX_min ? nw * NumCast(this.dataDoc._freeform_panX_min) : undefined; - this.dataDoc._freeform_panY_max = this.dataDoc._freeform_panY_max ? nw * NumCast(this.dataDoc._freeform_panY_max) : undefined; - this.dataDoc._freeform_panY_min = this.dataDoc._freeform_panY_min ? nw * NumCast(this.dataDoc._freeform_panY_min) : undefined; + this.dataDoc.freeform_panX = nw * NumCast(this.dataDoc.freeform_panX); + this.dataDoc.freeform_panY = nw * NumCast(this.dataDoc.freeform_panY); + this.dataDoc.freeform_panX_max = this.dataDoc.freeform_panX_max ? nw * NumCast(this.dataDoc.freeform_panX_max) : undefined; + this.dataDoc.freeform_panX_min = this.dataDoc.freeform_panX_min ? nw * NumCast(this.dataDoc.freeform_panX_min) : undefined; + this.dataDoc.freeform_panY_max = this.dataDoc.freeform_panY_max ? nw * NumCast(this.dataDoc.freeform_panY_max) : undefined; + this.dataDoc.freeform_panY_min = this.dataDoc.freeform_panY_min ? nw * NumCast(this.dataDoc.freeform_panY_min) : undefined; const newnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); DocListCast(this.dataDoc[this.annotationKey]).forEach(doc => { doc.x = (NumCast(doc.x) / oldnativeWidth) * newnativeWidth; doc.y = (NumCast(doc.y) / oldnativeWidth) * newnativeWidth; - if (!RTFCast(doc[Doc.LayoutFieldKey(doc)])) { + if (!RTFCast(doc[Doc.LayoutDataKey(doc)])) { doc.width = (NumCast(doc.width) / oldnativeWidth) * newnativeWidth; doc.height = (NumCast(doc.height) / oldnativeWidth) * newnativeWidth; } @@ -298,40 +299,38 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { crop = (region: Doc | undefined, addCrop?: boolean) => { if (!region) return undefined; const cropping = Doc.MakeCopy(region, true); - const regionData = region[DocData]; - regionData.lockedPosition = true; - regionData.title = 'region:' + this.Document.title; - regionData.followLinkToggle = true; + region.$lockedPosition = true; + region.$title = 'region:' + this.Document.title; + region.$followLinkToggle = true; this.addDocument(region); const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width); const anchh = NumCast(cropping._height); const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) / anchh; - cropping.title = 'crop: ' + this.Document.title; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); + cropping.onClick = undefined; cropping._width = anchw * (this._props.NativeDimScaling?.() || 1); cropping._height = anchh * (this._props.NativeDimScaling?.() || 1); - cropping.onClick = undefined; - const croppingProto = cropping[DocData]; - croppingProto.annotationOn = undefined; - croppingProto.isDataDoc = true; - croppingProto.backgroundColor = undefined; - croppingProto.proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO - croppingProto.type = DocumentType.IMG; - croppingProto.layout = ImageBox.LayoutString('data'); - croppingProto.data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField); - croppingProto.data_nativeWidth = anchw; - croppingProto.data_nativeHeight = anchh; - croppingProto.freeform_scale = viewScale; - croppingProto.freeform_panX = anchx / viewScale; - croppingProto.freeform_panY = anchy / viewScale; - croppingProto.freeform_scale_min = viewScale; - croppingProto.freeform_panX_min = anchx / viewScale; - croppingProto.freeform_panX_max = anchw / viewScale; - croppingProto.freeform_panY_min = anchy / viewScale; - croppingProto.freeform_panY_max = anchh / viewScale; + cropping.$title = 'crop: ' + this.Document.title; + cropping.$annotationOn = undefined; + cropping.$isDataDoc = true; + cropping.$backgroundColor = undefined; + cropping.$proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO + cropping.$type = DocumentType.IMG; + cropping.$layout = ImageBox.LayoutString('data'); + cropping.$data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField); + cropping.$data_nativeWidth = anchw; + cropping.$data_nativeHeight = anchh; + cropping.$freeform_scale = viewScale; + cropping.$freeform_panX = anchx / viewScale; + cropping.$freeform_panY = anchy / viewScale; + cropping.$freeform_scale_min = viewScale; + cropping.$freeform_panX_min = anchx / viewScale; + cropping.$freeform_panX_max = anchw / viewScale; + cropping.$freeform_panY_min = anchy / viewScale; + cropping.$freeform_panY_max = anchh / viewScale; if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); @@ -354,339 +353,141 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }); @observable _showOutpaintPrompt: boolean = false; -@observable _outpaintPromptInput: string = 'Extend this image naturally with matching content'; - -@action -openOutpaintPrompt = () => { - this._showOutpaintPrompt = true; -}; - -@action -closeOutpaintPrompt = () => { - this._showOutpaintPrompt = false; -}; + @observable _outpaintPromptInput: string = 'Extend this image naturally with matching content'; -@action -handlePromptChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this._outpaintPromptInput = e.target.value; -}; - -@action -submitOutpaintPrompt = () => { - this.closeOutpaintPrompt(); - this.processOutpaintingWithPrompt(this._outpaintPromptInput); -}; + @action + openOutpaintPrompt = () => { + this._showOutpaintPrompt = true; + }; -@action -processOutpaintingWithPrompt = async (customPrompt: string) => { - const field = Cast(this.dataDoc[this.fieldKey], ImageField); - if (!field) return; + @action + closeOutpaintPrompt = () => { + this._showOutpaintPrompt = false; + }; - const origWidth = NumCast(this.Document._outpaintingOriginalWidth); - const origHeight = NumCast(this.Document._outpaintingOriginalHeight); + @action + handlePromptChange = (val: string | number) => { + this._outpaintPromptInput = val; + }; - if (!origWidth || !origHeight) { - console.error('Original dimensions (_outpaintingOriginalWidth/_outpaintingOriginalHeight) not set. Ensure resizeViewForOutpainting was called first.'); - return; - } + @action + submitOutpaintPrompt = () => { + this.closeOutpaintPrompt(); + this.processOutpaintingWithPrompt(this._outpaintPromptInput); + }; - // Set flag that outpainting is in progress - this._outpaintingInProgress = true; + @action + processOutpaintingWithPrompt = async (customPrompt: string) => { + const field = Cast(this.dataDoc[this.fieldKey], ImageField); + if (!field) return; - try { - const currentPath = this.choosePath(field.url); - const newWidth = NumCast(this.Document._width); - const newHeight = NumCast(this.Document._height); + const origWidth = NumCast(this.Document._outpaintingOriginalWidth); + const origHeight = NumCast(this.Document._outpaintingOriginalHeight); - // Revert dimensions if prompt is blank (acts like Cancel) - if (!customPrompt) { - this.Document._width = origWidth; - this.Document._height = origHeight; - this._outpaintingInProgress = false; + if (!origWidth || !origHeight) { + console.error('Original dimensions (_outpaintingOriginalWidth/_outpaintingOriginalHeight) not set. Ensure resizeViewForOutpainting was called first.'); return; } - // Optional: add loading indicator - const loadingOverlay = document.createElement('div'); - loadingOverlay.style.position = 'absolute'; - loadingOverlay.style.top = '0'; - loadingOverlay.style.left = '0'; - loadingOverlay.style.width = '100%'; - loadingOverlay.style.height = '100%'; - loadingOverlay.style.background = 'rgba(0,0,0,0.5)'; - loadingOverlay.style.display = 'flex'; - loadingOverlay.style.justifyContent = 'center'; - loadingOverlay.style.alignItems = 'center'; - loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>'; - this._mainCont?.appendChild(loadingOverlay); - - const response = await Networking.PostToServer('/outpaintImage', { - imageUrl: currentPath, - prompt: customPrompt, - originalDimensions: { width: origWidth, height: origHeight }, - newDimensions: { width: newWidth, height: newHeight }, - }); - - if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') { - console.log('Received outpainted image:', response.url); + // Set flag that outpainting is in progress + this._outpaintingInProgress = true; - if (!this.dataDoc[this.fieldKey + '_alternates']) { - this.dataDoc[this.fieldKey + '_alternates'] = new List<Doc>(); + try { + const currentPath = this.choosePath(field.url); + const newWidth = NumCast(this.Document._width); + const newHeight = NumCast(this.Document._height); + + // Revert dimensions if prompt is blank (acts like Cancel) + if (!customPrompt) { + this.Document._width = origWidth; + this.Document._height = origHeight; + this._outpaintingInProgress = false; + return; } - const originalDoc = Docs.Create.ImageDocument(field.url.href, { - title: `Original: ${this.Document.title}`, - _nativeWidth: Doc.NativeWidth(this.dataDoc), - _nativeHeight: Doc.NativeHeight(this.dataDoc), + // Optional: add loading indicator + const loadingOverlay = document.createElement('div'); + loadingOverlay.style.position = 'absolute'; + loadingOverlay.style.top = '0'; + loadingOverlay.style.left = '0'; + loadingOverlay.style.width = '100%'; + loadingOverlay.style.height = '100%'; + loadingOverlay.style.background = 'rgba(0,0,0,0.5)'; + loadingOverlay.style.display = 'flex'; + loadingOverlay.style.justifyContent = 'center'; + loadingOverlay.style.alignItems = 'center'; + loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>'; + this._mainCont?.appendChild(loadingOverlay); + + const response = await Networking.PostToServer('/outpaintImage', { + imageUrl: currentPath, + prompt: customPrompt, + originalDimensions: { width: origWidth, height: origHeight }, + newDimensions: { width: newWidth, height: newHeight }, }); - Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', originalDoc); - - // Replace with new outpainted image - this.dataDoc[this.fieldKey] = new ImageField(response.url); - - Doc.SetNativeWidth(this.dataDoc, newWidth); - Doc.SetNativeHeight(this.dataDoc, newHeight); - - this.Document.ai = true; - this.Document.ai_outpainted = true; - this.Document.ai_outpaint_prompt = customPrompt; - } else { - console.error('Unexpected API response:', response); - this.Document._width = origWidth; - this.Document._height = origHeight; - alert('Failed to receive a valid image URL from server.'); - } - - this._mainCont?.removeChild(loadingOverlay); - } catch (error) { - console.error('Error during outpainting:', error); - this.Document._width = origWidth; - this.Document._height = origHeight; - alert('An error occurred while outpainting. Please try again.'); - } finally { - this._outpaintingInProgress = false; - delete this.Document._originalDims; - } -}; - -processOutpainting = () => { - this.openOutpaintPrompt(); -}; - -componentUI = () => ( - <> - {this._showOutpaintPrompt && ( - <div className="imageBox-regenerate-dialog"> - <h3>Outpaint Image</h3> - <p>Enter a prompt for extending the image:</p> - <input - type="text" - value={this._outpaintPromptInput} - onChange={this.handlePromptChange} - /> - <div className="buttons"> - <button onClick={this.closeOutpaintPrompt}>Cancel</button> - <button className="generate-btn" onClick={this.submitOutpaintPrompt}> - Generate - </button> - </div> - </div> - )} - </> -); - - // // Add this method to process outpainting when resize is complete - // @action - // processOutpainting = async () => { - // const field = Cast(this.dataDoc[this.fieldKey], ImageField); - // if (!field) return; - - // const origWidth = NumCast(this.Document._outpaintingOriginalWidth); - // const origHeight = NumCast(this.Document._outpaintingOriginalHeight); - - // if (!origWidth || !origHeight) { - // console.error('Original dimensions (_outpaintingOriginalWidth/_outpaintingOriginalHeight) not set. Ensure resizeViewForOutpainting was called first.'); - // return; - // } - - // //alert(`Original dimensions: ${origWidth} x ${origHeight}`); - - // // Set flag that outpainting is in progress - // this._outpaintingInProgress = true; - - // try { - // // Get the current path to the image - // const currentPath = this.choosePath(field.url); - - // // Get original and new dimensions for calculating mask - // const newWidth = NumCast(this.Document._width); - // const newHeight = NumCast(this.Document._height); - - // // Optional: Ask user for a prompt to guide the outpainting - // let prompt = 'Extend this image naturally with matching content'; - // const customPrompt = await new Promise<string>(resolve => { - // const dialog = document.createElement('div'); - // Object.assign(dialog.style, { - // position: 'fixed', - // top: '50%', - // left: '50%', - // transform: 'translate(-50%, -50%)', - // background: 'white', - // padding: '20px', - // borderRadius: '8px', - // boxShadow: '0 4px 12px rgba(0,0,0,0.2)', - // zIndex: '10000', - // }); - - // const title = document.createElement('h3'); - // title.style.marginTop = '0'; - // title.textContent = 'Outpaint Image'; - - // const description = document.createElement('p'); - // description.textContent = 'Enter a prompt for extending the image:'; + if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') { + console.log('Received outpainted image:', response.url); - // const input = document.createElement('input'); - // input.id = 'outpaint-prompt'; - // input.type = 'text'; - // input.value = 'Extend this image naturally with matching content'; - // Object.assign(input.style, { - // width: '300px', - // padding: '8px', - // marginBottom: '10px', - // }); - - // const buttonContainer = document.createElement('div'); - // Object.assign(buttonContainer.style, { - // display: 'flex', - // justifyContent: 'flex-end', - // gap: '10px', - // }); - - // const cancelButton = document.createElement('button'); - // cancelButton.textContent = 'Cancel'; - - // const confirmButton = document.createElement('button'); - // confirmButton.textContent = 'Generate'; - // Object.assign(confirmButton.style, { - // background: '#0078d4', - // color: 'white', - // border: 'none', - // padding: '8px 16px', - // }); - - // buttonContainer.appendChild(cancelButton); - // buttonContainer.appendChild(confirmButton); - - // dialog.appendChild(title); - // dialog.appendChild(description); - // dialog.appendChild(input); - // dialog.appendChild(buttonContainer); - - // document.body.appendChild(dialog); - - // cancelButton.onclick = () => { - // document.body.removeChild(dialog); - // resolve(''); - // }; - - // confirmButton.onclick = () => { - // const promptValue = input.value; - // document.body.removeChild(dialog); - // resolve(promptValue); - // }; - // }); - - // // If user cancelled, reset dimensions to original - // if (!customPrompt) { - // this.Document._width = origWidth; - // this.Document._height = origHeight; - // this._outpaintingInProgress = false; - // return; - // } - - // // Show loading indicator - // const loadingOverlay = document.createElement('div'); - // loadingOverlay.style.position = 'absolute'; - // loadingOverlay.style.top = '0'; - // loadingOverlay.style.left = '0'; - // loadingOverlay.style.width = '100%'; - // loadingOverlay.style.height = '100%'; - // loadingOverlay.style.background = 'rgba(0,0,0,0.5)'; - // loadingOverlay.style.display = 'flex'; - // loadingOverlay.style.justifyContent = 'center'; - // loadingOverlay.style.alignItems = 'center'; - // loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>'; - // this._mainCont?.appendChild(loadingOverlay); - - // // Call the outpaint API - // const response = await Networking.PostToServer('/outpaintImage', { - // imageUrl: currentPath, - // prompt: customPrompt, - // originalDimensions: { - // width: origWidth, - // height: origHeight, - // }, - // newDimensions: { - // width: newWidth, - // height: newHeight, - // }, - // }); - // if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') { - // console.log('Response is valid and contains URL:', response.url); - // } else { - // console.error('Unexpected API response:', response); - // alert('Failed to receive a valid image URL from server.'); - // } + if (!this.dataDoc[this.fieldKey + '_alternates']) { + this.dataDoc[this.fieldKey + '_alternates'] = new List<Doc>(); + } - // if (response && 'url' in response && typeof response.url === 'string') { - // // Save the original image as an alternate - // if (!this.dataDoc[this.fieldKey + '_alternates']) { - // this.dataDoc[this.fieldKey + '_alternates'] = new List<Doc>(); - // } + const originalDoc = Docs.Create.ImageDocument(field.url.href, { + title: `Original: ${this.Document.title}`, + _nativeWidth: Doc.NativeWidth(this.dataDoc), + _nativeHeight: Doc.NativeHeight(this.dataDoc), + }); - // // Create a copy of the current image as an alternate - // const originalDoc = Docs.Create.ImageDocument(field.url.href, { - // title: `Original: ${this.Document.title}`, - // _nativeWidth: Doc.NativeWidth(this.dataDoc), - // _nativeHeight: Doc.NativeHeight(this.dataDoc), - // }); + Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', originalDoc); - // // Add to alternates - // Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', originalDoc); + // Replace with new outpainted image + this.dataDoc[this.fieldKey] = new ImageField(response.url); - // // Update the image with the outpainted version - // this.dataDoc[this.fieldKey] = new ImageField(response.url); + Doc.SetNativeWidth(this.dataDoc, newWidth); + Doc.SetNativeHeight(this.dataDoc, newHeight); - // // Update native dimensions - // Doc.SetNativeWidth(this.dataDoc, newWidth); - // Doc.SetNativeHeight(this.dataDoc, newHeight); + this.Document.$ai = true; + this.Document.$ai_outpainted = true; + this.Document.$ai_outpaint_prompt = customPrompt; + } else { + console.error('Unexpected API response:', response); + this.Document._width = origWidth; + this.Document._height = origHeight; + alert('Failed to receive a valid image URL from server.'); + } - // // Add AI metadata - // this.Document.ai = true; - // this.Document.ai_outpainted = true; - // this.Document.ai_outpaint_prompt = customPrompt; - // } else { - // // If failed, revert to original dimensions - // this.Document._width = origWidth; - // this.Document._height = origHeight; - // alert('Failed to outpaint image. Please try again.'); - // } + this._mainCont?.removeChild(loadingOverlay); + } catch (error) { + console.error('Error during outpainting:', error); + this.Document._width = origWidth; + this.Document._height = origHeight; + alert('An error occurred while outpainting. Please try again.'); + } finally { + this._outpaintingInProgress = false; + } + }; - // // Remove loading overlay - // this._mainCont?.removeChild(loadingOverlay); - // } catch (error) { - // console.error('Error during outpainting:', error); - // // Revert to original dimensions on error - // this.Document._width = origWidth; - // this.Document._height = origHeight; - // alert('An error occurred while outpainting. Please try again.'); - // } finally { - // // Clear the outpainting flags - // this._outpaintingInProgress = false; - // delete this.Document._originalDims; - // } - // }; + processOutpainting = () => this.openOutpaintPrompt(); + + componentUI = () => + !this._showOutpaintPrompt ? null : ( + <div className="imageBox-regenerate-dialog" style={{ backgroundColor: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> + <h3>Outpaint Image</h3> + <EditableText + placeholder="Enter a prompt for extending the image:" + setVal={val => this.handlePromptChange(val)} + val={this._outpaintPromptInput} + type={Type.TERT} + color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} + /> + <div className="buttons"> + <Button text="Cancel" type={Type.TERT} onClick={this.closeOutpaintPrompt} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} /> + <Button text="Generate" type={Type.TERT} onClick={this.submitOutpaintPrompt} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} /> + </div> + </div> + ); specificContextMenu = (): void => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); @@ -698,9 +499,9 @@ componentUI = () => ( funcs.push({ description: 'GetImageText', event: () => { Networking.PostToServer('/queryFireflyImageText', { file: (file => { - const ext = extname(file); - return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); - })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href), + const ext = file ? extname(file) : ''; + return file?.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); + })(ImageCast(this.Document[Doc.LayoutDataKey(this.Document)])?.url.href), }).then(text => alert(text)); }, icon: 'expand-arrows-alt', @@ -726,14 +527,8 @@ componentUI = () => ( icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down', }); // Add new outpainting option - funcs.push({ - description: 'Outpaint Image', - event: () => { - this.openOutpaintPrompt(); - }, - icon: 'brush', - }); - + funcs.push({ description: 'Outpaint Image', event: () => this.openOutpaintPrompt(), icon: 'brush' }); + // Add outpainting history option if the image was outpainted this.Document.ai_outpainted && funcs.push({ @@ -752,15 +547,14 @@ componentUI = () => ( }; // updateIcon = () => new Promise<void>(res => res()); - updateIcon = (usePanelDimensions?: boolean) => { - const contentDiv = this._mainCont; - return !contentDiv + updateIcon = (/* usePanelDimensions?: boolean */) => + !this._mainCont || !DocListCast(this.dataDoc[this.annotationKey]).length ? new Promise<void>(res => res()) : UpdateIcon( this.layoutDoc[Id] + '_icon_' + new Date().getTime(), - contentDiv, - usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), - usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), + this._mainCont, + this._props.PanelWidth(), // usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), + this._props.PanelHeight(), // usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), this._props.PanelWidth(), this._props.PanelHeight(), 0, @@ -773,14 +567,13 @@ componentUI = () => ( this.dataDoc.icon_nativeHeight = nativeHeight; } ); - }; choosePath = (url: URL) => { if (!url?.href) return ''; const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return ClientUtils.CorsProxy(url.href); - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith('/assets/unknown-file-icon-hi.png')) return `/assets/unknown-file-icon-hi.png`; + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith(DefaultPath)) return DefaultPath; const ext = extname(url.href); return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); @@ -794,7 +587,7 @@ componentUI = () => ( @computed get nativeSize() { TraceMobx(); - if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; + if (this.paths.length && this.paths[0].includes(DefaultPath)) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)); const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)); const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1); @@ -804,15 +597,15 @@ componentUI = () => ( /** * How much the content of the view is being scaled based on its nesting and its fit-to-width settings */ - @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale * ( this._props.NativeDimScaling?.() || 1); } // prettier-ignore + @computed get viewScaling() { 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 maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.5 * Math.min(NumCast(this.Document.width)))* this.viewScaling; } // prettier-ignore + @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.2 * this._props.PanelWidth())*this.viewScaling; } // prettier-ignore /** * 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 uiBtnScaling() { return Math.min(this.maxWidgetSize / this._sideBtnWidth, 1); } // prettier-ignore + @computed get uiBtnScaling() { return Math.min(1/(this._props.NativeDimScaling?.()??1), this.maxWidgetSize / this._sideBtnWidth); } // prettier-ignore @computed get overlayImageIcon() { const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`]; @@ -859,30 +652,33 @@ componentUI = () => ( } @computed get regenerateImageIcon() { return ( - <div - className="imageBox-regenerateDropTarget" - ref={this._regenerateIconRef} - onClick={() => DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs), { openLocation: OpenWhere.addRight })} - style={{ - display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', - transform: `scale(${this.uiBtnScaling})`, - width: this._sideBtnWidth, - height: this._sideBtnWidth, - background: 'transparent', - // color: SettingsManager.userBackgroundColor, - }}> - {this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width="100%" height="100%" /> : <FontAwesomeIcon icon="portrait" color={SettingsManager.userColor} size="lg" />} - </div> + <Tooltip title={'click to show AI generations. Drop an image on to create a new generation'}> + <div + className="imageBox-regenerateDropTarget" + ref={this._regenerateIconRef} + onClick={() => DocCast(this.Document.ai_firefly_generatedDocs) && DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs)!, { openLocation: OpenWhere.addRight })} + style={{ + display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', + transform: `scale(${this.uiBtnScaling})`, + width: this._sideBtnWidth, + height: this._sideBtnWidth, + background: 'black', + color: 'white', + // color: SettingsManager.userBackgroundColor, + }}> + {this._regenerateLoading ? <ReactLoading type="spin" width="100%" height="100%" /> : <FontAwesomeIcon icon="portrait" size="lg" />} + </div> + </Tooltip> ); } @computed get paths() { const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); // retrieve the primary image URL that is being rendered from the data doc const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images - const defaultUrl = new URL(ClientUtils.prepend('/assets/unknown-file-icon-hi.png')); + const defaultUrl = new URL(ClientUtils.prepend(DefaultPath)); const altpaths = alts - ?.map(doc => (doc instanceof Doc ? (ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl) : defaultUrl)) + ?.map(doc => (doc instanceof Doc ? (ImageCast(doc[Doc.LayoutDataKey(doc)])?.url ?? defaultUrl) : defaultUrl)) .filter(url => url) .map(url => this.choosePath(url)) ?? []; // acc ess the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; @@ -892,11 +688,15 @@ componentUI = () => ( @computed get content() { TraceMobx(); + const usePath = StrCast(this.Document[this.fieldKey + '_usePath']); + const alternate = '_' + usePath.replace(':hover', ''); + const altColor = DashColor(StrCast(this.Document[this.fieldKey + alternate], StrCast(this.Document['$backgroundColor' + alternate], 'black'))); + const backColor = DashColor((this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string) ?? Colors.WHITE); // allow use case where the image is transparent when the alpha value is to smallest possible value from UI (alpha = 1 out of 255) const backAlpha = backColor.alpha() < 0.015 && backColor.alpha() > 0 ? backColor.alpha() : 1; - const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0]; - const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); + const fadepath = this.layoutDoc.hideImage ? '' : this.paths[0]; + const srcpath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); const { nativeWidth, nativeHeight /* , nativeOrientation */ } = this.nativeSize; const rotation = NumCast(this.dataDoc[this.fieldKey + '_rotation']); const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1; @@ -930,15 +730,15 @@ componentUI = () => ( key="paths" src={srcpath} style={{ transform, transformOrigin }} - onError={action(e => { - this._error = e.toString(); - })} + onError={action(e => (this._error = e.toString()))} draggable={false} width={nativeWidth} /> {fadepath === srcpath ? null : ( - <div className={`imageBox-fadeBlocker${this.usingAlternate ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}> - <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={20} /> + <div + className={`imageBox-fadeBlocker${!this.usingAlternate ? '-hover' : ''}`} + style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms'), background: altColor.alpha() === 0 ? 'transparent' : altColor.toString() }}> + <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> </div> )} </div> @@ -1001,7 +801,7 @@ componentUI = () => ( onClick={action(async () => { this._regenerateLoading = true; if (this._fireflyRefStrength) { - DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title), this.Document)?.then(action(() => (this._regenerateLoading = false))); + DrawingFillHandler.drawingToImage(this.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title))?.then(action(() => (this._regenerateLoading = false))); } else { SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then( action(newImgs => { @@ -1010,7 +810,7 @@ componentUI = () => ( const url = firstImg.pathname; const imgField = new ImageField(url); this._prevImgs.length === 0 && - this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname }); + this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field?.url.pathname ?? '' }); this._prevImgs.unshift({ prompt: firstImg.prompt, seed: firstImg.seed, pathname: url }); this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs); this.dataDoc.ai_firefly_prompt = firstImg.prompt; @@ -1055,21 +855,24 @@ componentUI = () => ( } screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale); marqueeDown = (e: React.PointerEvent) => { - if (!this.dataDoc[this.fieldKey]) { - this.chooseImage(); - } else if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { - setupMoveUpEvents( - this, - e, - action(moveEv => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); - return true; - }), - returnFalse, - () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), - false - ); + if (!e.altKey && e.button === 0) { + if (NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { + setupMoveUpEvents( + this, + e, + action(moveEv => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); + return true; + }), + returnFalse, + () => { + if (!this.dataDoc[this.fieldKey]) this.chooseImage(); + else MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + }, + false + ); + } } }; @action @@ -1094,8 +897,6 @@ componentUI = () => ( TraceMobx(); const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string; const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad; - const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); - const doc = this.usingAlternate ? (alts.lastElement() ?? this.Document) : this.Document; return ( <> <div @@ -1121,7 +922,7 @@ componentUI = () => ( <CollectionFreeFormView ref={this._ffref} {...this._props} - Document={doc} + Document={this.Document} setContentViewBox={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} @@ -1144,41 +945,12 @@ componentUI = () => ( addDocument={this.addDocument}> {this.content} </CollectionFreeFormView> - {this.Loading ? ( - <div className="loading-spinner" style={{ position: 'absolute' }}> - <ReactLoading type="spin" height={50} width={50} color={'blue'} /> + {this._outpaintingInProgress && ( + <div className="imageBox-outpaintingSpinner"> + <ReactLoading type="spin" color="#666" height={60} width={60} /> </div> - ) : null} - {this.regenerateImageIcon} - {this.overlayImageIcon} - {this.annotationLayer} - {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : ( - <MarqueeAnnotator - Document={this.Document} - ref={this.marqueeref} - scrollTop={0} - annotationLayerScrollTop={0} - scaling={returnOne} - annotationLayerScaling={this._props.NativeDimScaling} - screenTransform={this.DocumentView().screenToViewTransform} - docView={this.DocumentView} - addDocument={this.addDocument} - finishMarquee={this.finishMarquee} - savedAnnotations={this.savedAnnotations} - selectionText={returnEmptyString} - annotationLayer={this._annotationLayer.current} - marqueeContainer={this._mainCont} - highlightDragSrcColor="" - anchorMenuCrop={this.crop} - // anchorMenuFlashcard={() => this.getImageDesc()} - /> )} </div> - {this._outpaintingInProgress && ( - <div className="imageBox-outpaintingSpinner"> - <ReactLoading type="spin" color="#666" height={60} width={60} /> - </div> - )} </> ); } diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 40c687b7e..aa66b5ba9 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -9,7 +9,7 @@ import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { DocCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { DocumentType } from '../../documents/DocumentTypes'; -import { Docs } from '../../documents/Documents'; +import { Docs, DocumentOptions } from '../../documents/Documents'; import { SetupDrag } from '../../util/DragManager'; import { CompiledScript } from '../../util/Scripting'; import { undoable } from '../../util/UndoManager'; @@ -22,6 +22,7 @@ import './KeyValueBox.scss'; import { KeyValuePair } from './KeyValuePair'; import { OpenWhere } from './OpenWhere'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { DocLayout } from '../../../fields/DocSymbols'; export type KVPScript = { script: CompiledScript; @@ -54,16 +55,12 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable private rows: KeyValuePair[] = []; @observable _splitPercentage = 50; - get fieldDocToLayout() { - return DocCast(this.Document); - } - @action onEnterKey = (e: React.KeyboardEvent): void => { if (e.key === 'Enter') { e.stopPropagation(); - if (this._keyInput.current?.value && this._valInput.current?.value && this.fieldDocToLayout) { - if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput.current.value, this._valInput.current.value)) { + if (this._keyInput.current?.value && this._valInput.current?.value && this.Document) { + if (KeyValueBox.SetField(this.Document, this._keyInput.current.value, this._valInput.current.value)) { this._keyInput.current.value = ''; this._valInput.current.value = ''; document.body.focus(); @@ -95,10 +92,11 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { return !script.compiled ? undefined : { script, type, onDelegate }; }; - public static ApplyKVPScript = (doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => { + public static ApplyKVPScript = (doc: Doc, keyIn: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => { const { script, type, onDelegate } = kvpScript; - // const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates - const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc); + const chooseDelegate = forceOnDelegate || onDelegate || keyIn.startsWith('_'); + const key = chooseDelegate && keyIn.startsWith('$') ? keyIn.slice(1) : keyIn; + const target = chooseDelegate ? doc : DocCast(doc.proto, doc)!; let field: FieldType | undefined; switch (type) { case 'computed': field = new ComputedField(script); break; // prettier-ignore @@ -106,15 +104,15 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { default: { const _setCacheResult_ = (value: FieldResult) => { field = value as FieldType; - if (setResult) setResult?.(value); + if (setResult) setResult(value); else target[key] = field; }; - const res = script.run({ this: Doc.Layout(doc), _setCacheResult_ }, console.log); + const res = script.run({ this: doc, _setCacheResult_ }, console.log); if (!res.success) { if (key) target[key] = script.originalScript; return false; } - field === undefined && (field = res.result instanceof Array ? new List<FieldType>(res.result) : (typeof res.result === 'function' ? res.result.name : res.result as FieldType)); + field === undefined && (field = res.result instanceof Array ? new List<FieldType>(res.result) : typeof res.result === 'function' ? res.result.name : (res.result as FieldType)); } } if (!key) return false; @@ -125,10 +123,16 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { return false; }; - public static SetField = undoable((doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => { + public static SetField = undoable((doc: Doc, key: string, value: string, forceOnDelegateIn?: boolean, setResult?: (value: FieldResult) => void) => { const script = KeyValueBox.CompileKVPScript(value); if (!script) return false; - return KeyValueBox.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult); + const ldoc = key.startsWith('_') ? doc[DocLayout] : doc; + const forceOnDelegate = forceOnDelegateIn || (ldoc !== doc && !value.startsWith('=')); + // an '=' value redirects a key targeting the render template to the root document. + // also, if ldoc and doc are equal, allow redirecting to data document when not using an equal + // in either case, get rid of initial '_' which forces writing to layout + const setKey = value.startsWith('=') || ldoc === doc ? key.replace(/^_/, '') : key; + return KeyValueBox.ApplyKVPScript(doc, setKey, script, forceOnDelegate, setResult); }, 'Set Doc Field'); onPointerDown = (e: React.PointerEvent): void => { @@ -141,7 +145,7 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { rowHeight = () => 30; @computed get createTable() { - const doc = this.fieldDocToLayout; + const doc = this.Document; if (!doc) { return ( <tr> @@ -149,25 +153,39 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { </tr> ); } - const realDoc = doc; const ids: { [key: string]: string } = {}; const protos = Doc.GetAllPrototypes(doc); protos.forEach(proto => { Object.keys(proto).forEach(key => { - if (!(key in ids) && realDoc[key] !== ComputedField.undefined) { + if (!(key in ids) && doc[key] !== ComputedField.undefined) { ids[key] = key; } }); }); + const docinfos = new DocumentOptions(); + + const layoutProtos = this.layoutDoc !== doc ? Doc.GetAllPrototypes(this.layoutDoc) : []; + layoutProtos.forEach(proto => { + Object.keys(proto) + .filter(key => !(key in ids) || docinfos['_' + key]) // if '_key' is in docinfos (as opposed to just 'key'), then the template Doc's value should be displayed instead of the root document's value + .map(key => '_' + key) + .forEach(key => { + if (doc[key] !== ComputedField.undefined) { + if (key.replace(/^_/, '') in ids) delete ids[key.replace(/^_/, '')]; + ids[key] = key; + } + }); + }); + const rows: JSX.Element[] = []; let i = 0; const keys = Object.keys(ids).slice(); // for (const key of [...keys.filter(id => id !== 'layout' && !id.includes('_')).sort(), ...keys.filter(id => id === 'layout' || id.includes('_')).sort()]) { const sortedKeys = keys.sort((a: string, b: string) => { - const a_ = a.split('_')[0]; - const b_ = b.split('_')[0]; + const a_ = a.replace(/^_/, '').split('_')[0]; + const b_ = b.replace(/^_/, '').split('_')[0]; if (a_ < b_) return -1; if (a_ > b_) return 1; if (a === a_) return -1; @@ -177,7 +195,7 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { sortedKeys.forEach(key => { rows.push( <KeyValuePair - doc={realDoc} + doc={doc} addDocTab={this._props.addDocTab} PanelWidth={this._props.PanelWidth} PanelHeight={this.rowHeight} @@ -243,15 +261,15 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { getFieldView = () => { const rows = this.rows.filter(row => row.isChecked); if (rows.length > 1) { - const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.Document).title}`, _chromeHidden: true }); + const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${this.Document.title}`, _chromeHidden: true }); rows.forEach(row => { - const field = this.createFieldView(DocCast(this.Document), row); + const field = this.createFieldView(this.Document, row); field && Doc.AddDocToList(parent, 'data', field); row.uncheck(); }); return parent; } - return rows.length ? this.createFieldView(DocCast(this.Document), rows.lastElement()) : undefined; + return rows.length ? this.createFieldView(this.Document, rows.lastElement()) : undefined; }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { @@ -300,7 +318,7 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { description: 'Default Perspective', event: () => { this._props.addDocTab(this.Document, OpenWhere.close); - this._props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight); + this._props.addDocTab(this.Document, OpenWhere.addRight); }, icon: 'image', }); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 85aff04c3..d8f968a40 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -16,6 +16,7 @@ import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; import './KeyValueBox.scss'; import './KeyValuePair.scss'; import { OpenWhere } from './OpenWhere'; +import { DocLayout } from '../../../fields/DocSymbols'; // Represents one row in a key value plane @@ -60,30 +61,23 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { }; render() { - // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; + let doc: Doc | undefined = this._props.keyName.startsWith('_') ? this._props.doc[DocLayout] : this._props.doc; + const layoutField = doc !== this._props.doc; + const key = layoutField ? this._props.keyName.replace(/^_/, '') : this._props.keyName; let protoCount = 0; - let { doc } = this._props; - while (doc) { - if (Object.keys(doc).includes(this._props.keyName)) { - break; - } + while (doc && !Object.keys(doc).includes(key)) { protoCount++; doc = DocCast(doc.proto); } const parenCount = Math.max(0, protoCount - 1); - const keyStyle = protoCount === 0 ? 'black' : 'blue'; - + const keyStyle = layoutField ? 'red' : protoCount === 0 ? 'black' : 'blue'; const hover = { transition: '0.3s ease opacity', opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; - + const docOpts = Object.entries(new DocumentOptions()); return ( <tr - className={this._props.rowStyle} - onPointerEnter={action(() => { - this.isPointerOver = true; - })} - onPointerLeave={action(() => { - this.isPointerOver = false; - })}> + className={this._props.rowStyle} // + onPointerEnter={action(() => (this.isPointerOver = true))} + onPointerLeave={action(() => (this.isPointerOver = false))}> <td className="keyValuePair-td-key" style={{ width: `${this._props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> <button @@ -91,17 +85,15 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { style={hover} className="keyValuePair-td-key-delete" onClick={undoable(() => { - if (Object.keys(this._props.doc).indexOf(this._props.keyName) !== -1) { - delete this._props.doc[this._props.keyName]; - } else delete DocCast(this._props.doc.proto)?.[this._props.keyName]; + doc && delete (Object.keys(doc).indexOf(key) !== -1 ? doc : DocCast(this._props.doc.proto)!)[key]; }, 'set key value')}> X </button> <input className="keyValuePair-td-key-check" type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} /> - <Tooltip title={Object.entries(new DocumentOptions()).find((pair: [string, FInfo]) => pair[0].replace(/^_/, '') === this._props.keyName)?.[1].description ?? ''}> - <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (this._props.keyName.replace(/__/g, '').match(/_/g)?.length || 0), color: keyStyle }}> + <Tooltip title={(docOpts.find(([k]) => k.replace(/^_/, '') === key)?.[1] as FInfo)?.description ?? ''}> + <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (key.match(/_/g)?.length || 0), color: keyStyle }}> {'('.repeat(parenCount)} - {this._props.keyName} + {(keyStyle === 'black' ? '' : layoutField ? '_' : '$') + key} {')'.repeat(parenCount)} </div> </Tooltip> @@ -133,7 +125,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { pinToPres: returnZero, }} GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)} - SetValue={(value: string) => Doc.SetField(this._props.doc, this._props.keyName, value)} + SetValue={value => Doc.SetField(this._props.doc, this._props.keyName, value)} /> </div> </td> diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 7fb83571f..b08ed84b7 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -13,11 +13,11 @@ import { undoable } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; +import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import './LabelBox.scss'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { RichTextMenu } from './formattedText/RichTextMenu'; -import { DocumentView } from './DocumentView'; @observer export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -103,11 +103,11 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { }; if (r) { if (!r.offsetHeight || !r.offsetWidth) { - //console.log("CAN'T FIT TO EMPTY BOX"); - this._timeout && clearTimeout(this._timeout); + r.style.opacity = '0'; this._timeout = setTimeout(() => this.fitTextToBox(r)); return textfitParams; } + r.style.opacity = '1'; r.style.whiteSpace = ''; // textfit sets to nowrap if not multiline, but doesn't reeset if it becomes multiline r.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']); // textfit doesn't reset textAlign if it has been set to center, so we just set it to what we want r.firstChild instanceof HTMLElement && (r.firstChild.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align'])); @@ -222,7 +222,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { FormattedTextBox.LiveTextUndo = undefined; }} dangerouslySetInnerHTML={{ - __html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title.startsWith('#') ? null : (this.Title ?? '')}</span>`, + __html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title?.startsWith('#') ? '' : (this.Title ?? '')}</span>`, }} contentEditable={this._props.onClickScript?.() ? undefined : true} ref={r => { @@ -239,7 +239,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.Title) { this.resetCursor(); } - } + } else this._timeout && clearTimeout(this._timeout); }} /> </div> diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index d5dc256d9..8bf65b637 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -252,7 +252,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { fontSize={fontSize} GetValue={() => linkDesc} SetValue={action(val => { - this.Document[DocData].link_description = val; + this.Document.$link_description = val; return true; })} /> @@ -262,8 +262,8 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { background={color} color={fontColor || lightOrDark(DashColor(color).fade(0.5).toString())} type={Type.PRIM} - val={StrCast(this.Document[DocData].link_description)} - setVal={action(val => (this.Document[DocData].link_description = val))} + val={StrCast(this.Document.$link_description)} + setVal={action(val => (this.Document.$link_description = val))} fillWidth /> */} </div> diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index ff95f8547..aeac100f4 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -1,25 +1,23 @@ import { action, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { DocData } from '../../../fields/DocSymbols'; import { StrCast } from '../../../fields/Types'; import { LinkManager } from '../../util/LinkManager'; import './LinkDescriptionPopup.scss'; import { TaskCompletionBox } from './TaskCompletedBox'; @observer -export class LinkDescriptionPopup extends React.Component<{}> { +export class LinkDescriptionPopup extends React.Component<object> { // eslint-disable-next-line no-use-before-define public static Instance: LinkDescriptionPopup; @observable public display: boolean = false; - // eslint-disable-next-line react/no-unused-class-component-methods @observable public showDescriptions: string = 'ON'; @observable public popupX: number = 700; @observable public popupY: number = 350; @observable description: string = ''; @observable popupRef = React.createRef<HTMLDivElement>(); - constructor(props: any) { + constructor(props: object) { super(props); makeObservable(this); LinkDescriptionPopup.Instance = this; @@ -47,15 +45,13 @@ export class LinkDescriptionPopup extends React.Component<{}> { @action onDismiss = (add: boolean) => { this.display = false; - if (add) { - LinkManager.Instance.currentLink && (LinkManager.Instance.currentLink[DocData].link_description = this.description); - } + add && LinkManager.Instance.currentLink && (LinkManager.Instance.currentLink.$link_description = this.description); this.description = ''; }; @action onClick = (e: PointerEvent) => { - if (this.popupRef && !this.popupRef.current?.contains(e.target as any)) { + if (this.popupRef && !this.popupRef.current?.contains(e.target as Node)) { this.display = false; this.description = ''; TaskCompletionBox.taskCompleted = false; diff --git a/src/client/views/nodes/MapBox/AnimationUtility.ts b/src/client/views/nodes/MapBox/AnimationUtility.ts index a3ac68b99..f6509b885 100644 --- a/src/client/views/nodes/MapBox/AnimationUtility.ts +++ b/src/client/views/nodes/MapBox/AnimationUtility.ts @@ -3,7 +3,7 @@ import * as d3 from 'd3'; import { Feature, GeoJsonProperties, Geometry, LineString } from 'geojson'; import { MercatorCoordinate } from 'mapbox-gl'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; -import { MapRef } from 'react-map-gl'; +import { MapRef } from 'react-map-gl/mapbox'; export type Position = [number, number]; diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx index 8079d96ea..fc5377ba4 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -1,7 +1,8 @@ +import { IconButton } from '@dash/components'; import { IconLookup, faAdd, faArrowDown, faArrowLeft, faArrowsRotate, faBicycle, faCalendarDays, faCar, faDiamondTurnRight, faEdit, faPersonWalking, faRoute } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/material'; -import { IconButton } from '@dash/components'; +import { LngLatLike } from 'mapbox-gl'; import { IReactionDisposer, ObservableMap, action, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -14,12 +15,10 @@ import { CalendarManager } from '../../../util/CalendarManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { DocumentView } from '../DocumentView'; +import { Position } from './AnimationUtility'; import './MapAnchorMenu.scss'; import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; import { MarkerIcons } from './MarkerIcons'; -import { LngLatLike } from 'mapbox-gl'; -import { Position } from './AnimationUtility'; -// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; type MapAnchorMenuType = 'standard' | 'routeCreation' | 'calendar' | 'customize' | 'route'; @@ -46,7 +45,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public IsTargetToggler: () => boolean = returnFalse; public DisplayRoute: (routeInfoMap: Record<TransportationType, { coordinates: Position[] }> | undefined, type: TransportationType) => void = unimplementedFunction; public AddNewRouteToMap: (coordinates: Position[], origin: string, destination: { place_name: string; center: number[] }, createPinForDestination: boolean) => void = unimplementedFunction; - public CreatePin: (feature: { place_name: string; center: LngLatLike; properties: { wikiData: unknown } }) => void = unimplementedFunction; + public CreatePin: (feature: { place_name: string; center: LngLatLike; properties?: { wikiData: string } }) => void = unimplementedFunction; public UpdateMarkerColor: (color: string) => void = unimplementedFunction; public UpdateMarkerIcon: (iconKey: string) => void = unimplementedFunction; @@ -293,7 +292,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { return undefined; }; - getDirectionsButton: JSX.Element = (<IconButton tooltip="Get directions" onPointerDown={this.DirectionsClick} icon={<FontAwesomeIcon icon={faDiamondTurnRight as IconLookup} />} color={SettingsManager.userColor} />); + getDirectionsButton = () => <IconButton tooltip="Get directions" onPointerDown={this.DirectionsClick} icon={<FontAwesomeIcon icon={faDiamondTurnRight as IconLookup} />} color={SettingsManager.userColor} />; getAddToCalendarButton = (docType: string): JSX.Element => ( <IconButton @@ -305,9 +304,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { color={SettingsManager.userColor} /> ); - addToCalendarButton: JSX.Element = ( - <IconButton tooltip="Add to calendar" onPointerDown={() => CalendarManager.Instance.open(undefined, this.pinDoc)} icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} /> - ); + addToCalendarButton = () => <IconButton tooltip="Add to calendar" onPointerDown={() => CalendarManager.Instance.open(undefined, this.pinDoc)} icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} />; getLinkNoteToDocButton = (docType: string): JSX.Element => ( <div ref={this._commentRef}> @@ -320,7 +317,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { </div> ); - linkNoteToPinOrRoutenButton: JSX.Element = ( + linkNoteToPinOrRoutenButton = () => ( <div ref={this._commentRef}> <IconButton tooltip="Link Note to Pin" // @@ -331,9 +328,9 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { </div> ); - customizePinButton: JSX.Element = (<IconButton tooltip="Customize pin" onPointerDown={this.CustomizeClick} icon={<FontAwesomeIcon icon={faEdit as IconLookup} />} color={SettingsManager.userColor} />); + customizePinButton = () => <IconButton tooltip="Customize pin" onPointerDown={this.CustomizeClick} icon={<FontAwesomeIcon icon={faEdit as IconLookup} />} color={SettingsManager.userColor} />; - centerOnPinButton: JSX.Element = ( + centerOnPinButton = () => ( <IconButton tooltip="Center on pin" // onPointerDown={this.Center} @@ -342,7 +339,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { /> ); - backButton: JSX.Element = ( + backButton = () => ( <IconButton tooltip="Go back" // onPointerDown={this.BackClick} @@ -351,7 +348,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { /> ); - addRouteButton: JSX.Element = ( + addRouteButton = () => ( <IconButton tooltip="Add route" // onPointerDown={this.HandleAddRouteClick} @@ -369,9 +366,9 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { /> ); - animateRouteButton: JSX.Element = (<IconButton tooltip="Animate route" onPointerDown={() => this.OpenAnimationPanel(this.routeDoc)} icon={<FontAwesomeIcon icon={faRoute as IconLookup} />} color={SettingsManager.userColor} />); + animateRouteButton = () => <IconButton tooltip="Animate route" onPointerDown={() => this.OpenAnimationPanel(this.routeDoc)} icon={<FontAwesomeIcon icon={faRoute as IconLookup} />} color={SettingsManager.userColor} />; - revertToOriginalMarkerButton = ( + revertToOriginalMarkerButton = () => ( <IconButton tooltip="Revert to original" // onPointerDown={() => this.revertToOriginalMarker()} @@ -386,31 +383,31 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { {this.menuType === 'standard' && ( <> {this.getDeleteButton('pin')} - {this.getDirectionsButton} + {this.getDirectionsButton()} {this.getAddToCalendarButton('pin')} {this.getLinkNoteToDocButton('pin')} - {this.customizePinButton} - {this.centerOnPinButton} + {this.customizePinButton()} + {this.centerOnPinButton()} </> )} {this.menuType === 'routeCreation' && ( <> - {this.backButton} - {this.addRouteButton} + {this.backButton()} + {this.addRouteButton()} </> )} {this.menuType === 'route' && ( <> {this.getDeleteButton('route')} - {this.animateRouteButton} + {this.animateRouteButton()} {this.getAddToCalendarButton('route')} {this.getLinkNoteToDocButton('route')} </> )} {this.menuType === 'customize' && ( <> - {this.backButton} - {this.revertToOriginalMarkerButton} + {this.backButton()} + {this.revertToOriginalMarkerButton()} </> )} diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index fdd8a29d7..bd4b51038 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -7,6 +7,9 @@ overflow: hidden; display: flex; position: absolute; + .mapboxgl-marker { + cursor: default; + } .mapboxgl-map { overflow: unset !important; @@ -27,6 +30,9 @@ gap: 5px; align-items: center; width: calc(100% - 40px); + z-index: 1; + position: relative; + background: lightGray; } .mapbox-settings-panel { @@ -171,6 +177,8 @@ .mapBox-wrapper { width: 100%; + transform-origin: top left; + .mapBox-input { box-sizing: border-box; border: 1px solid transparent; diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 792cb6b46..a563b7c1b 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -1,8 +1,8 @@ -import { IconLookup, faCircleXmark, faGear, faPause, faPlay, faRotate } from '@fortawesome/free-solid-svg-icons'; +import { IconButton, Size, Type } from '@dash/components'; +import { faCircleXmark, faGear, faPause, faPlay, faRotate } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, FormControlLabel, TextField } from '@mui/material'; import * as turf from '@turf/turf'; -import { IconButton, Size, Type } from '@dash/components'; import * as d3 from 'd3'; import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString } from 'geojson'; import { LngLatBoundsLike, LngLatLike, MapLayerMouseEvent } from 'mapbox-gl'; @@ -10,11 +10,14 @@ import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObserv import { observer } from 'mobx-react'; import * as React from 'react'; import { CirclePicker, ColorResult } from 'react-color'; -import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl'; +import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl/mapbox'; import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; -import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, LinkedTo, Opt, StrListCast } from '../../../../fields/Doc'; +import { List } from '../../../../fields/List'; +import { RichTextField } from '../../../../fields/RichTextField'; import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types'; +import { TraceMobx } from '../../../../fields/util'; import { DocUtils } from '../../../documents/DocUtils'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; @@ -34,7 +37,6 @@ import { MapAnchorMenu } from './MapAnchorMenu'; import './MapBox.scss'; import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; import { MarkerIcons } from './MarkerIcons'; -import { RichTextField } from '../../../../fields/RichTextField'; // import { GeocoderControl } from './GeocoderControl'; // amongus @@ -76,7 +78,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { makeObservable(this); } - @observable _featuresFromGeocodeResults: { place_name: string; center: LngLatLike | undefined }[] = []; + @observable _featuresFromGeocodeResults: { place_name: string; center: LngLatLike | undefined; properties?: { wikiData: string } }[] = []; @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable _selectedPinOrRoute: Doc | undefined = undefined; // The pin that is selected @observable _mapReady = false; @@ -123,7 +125,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const originalCoordinates: Position[] = JSON.parse(StrCast(this._routeToAnimate.routeCoordinates)); // const index = Math.floor(this.animationPhase * originalCoordinates.length); const index = this._animationPhase * (originalCoordinates.length - 1); // Calculate the fractional index - console.log('Animation phase', this._animationPhase); const startIndex = Math.floor(index); const endIndex = Math.ceil(index); let feature: Feature<Geometry, GeoJsonProperties>; @@ -183,7 +184,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }); return feature; } - console.log('ERROR'); return { type: 'Feature', properties: {}, @@ -199,7 +199,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @computed get allRoutesGeoJson(): FeatureCollection { const features: Feature<Geometry, GeoJsonProperties>[] = this.allRoutes.map((routeDoc: Doc) => { - console.log('Route coords: ', routeDoc.routeCoordinates); const geometry: LineString = { type: 'LineString', coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)), @@ -215,7 +214,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return { type: 'FeatureCollection', - features: features, + features, }; } @@ -241,7 +240,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { toList(docs).forEach(doc => { let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this._selectedPinOrRoute; if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) { - existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map)); + existingPin = this.createPushpin({ lng: NumCast(doc.longitude), lat: NumCast(doc.latitude) }, StrCast(doc.map)); } if (existingPin) { setTimeout(() => { @@ -477,7 +476,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action deleteSelectedPinOrRoute = undoable(() => { - console.log('deleting'); if (this._selectedPinOrRoute) { // Removes filter Doc.setDocFilter(this.Document, 'latitude', NumCast(this._selectedPinOrRoute.latitude), 'remove'); @@ -542,17 +540,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { * Creates Pushpin doc and adds it to the list of annotations */ @action - createPushpin = undoable((center: LngLatLike, location?: string, wikiData?: string) => { - const lat = 'lat' in center ? center.lat : center[0]; - const lon = 'lng' in center ? center.lng : 'lon' in center ? center.lon : center[1]; + createPushpin = (center: LngLatLike, location?: string, wikiData?: string) => { + const [lng, lat] = center instanceof Array ? center : ['lng' in center ? center.lng : center.lon, center.lat]; // Stores the pushpin as a MapMarkerDocument const pushpin = Docs.Create.PushpinDocument( lat, - lon, + lng, false, [], { - title: location ?? `lat=${lat},lng=${lon}`, + title: location ?? `lat=${lat},lng=${lng}`, map: location, description: '', wikiData: wikiData, @@ -563,11 +560,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // ,'pushpinIDamongus'+ this.incrementer++ ); this.addDocument(pushpin, this.annotationKey); - console.log(pushpin); return pushpin; // mapMarker.infoWindowOpen = true; - }, 'createpin'); + }; @action createMapRoute = undoable((coordinates: Position[], originName: string, destination: { place_name: string; center: number[] }, createPinForDestination: boolean) => { @@ -575,7 +571,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const mapRoute = Docs.Create.MapRouteDocument(false, [], { title: `${originName} --> ${destination.place_name}`, routeCoordinates: JSON.stringify(coordinates) }); this.addDocument(mapRoute, this.annotationKey); if (createPinForDestination) { - this.createPushpin(destination.center[1], destination.center[0], destination.place_name); + this.createPushpin({ lng: destination.center[0], lat: destination.center[1] }, destination.place_name); } this._temporaryRouteSource = { type: 'FeatureCollection', @@ -598,18 +594,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; @action - addMarkerForFeature = (feature: { place_name: string; center: LngLatLike | undefined; properties?: { wikiData: unknown } }) => { - const location = feature.place_name; + addMarkerForFeature = (feature: { place_name: string; center: LngLatLike | undefined; properties?: { wikiData: string } }) => { if (feature.center) { - const wikiData = feature.properties?.wikiData; - - this.createPushpin(feature.center, location, wikiData); - - if (this._mapRef.current) { - this._mapRef.current.flyTo({ - center: feature.center, - }); - } + this.createPushpin(feature.center, feature.place_name, feature.properties?.wikiData); + this._mapRef.current?.flyTo({ + center: feature.center, + }); this._featuresFromGeocodeResults = []; } else { // TODO: handle error @@ -632,7 +622,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // try { // const url = MAPBOX_FORWARD_GEOCODE_BASE_URL + encodeURI(searchText) +'.json' +`?access_token=${MAPBOX_ACCESS_TOKEN}`; // const response = await fetch(url); - // const data = await response.json(); + // const data = await response.jchildDocson(); // runInAction(() => { // this.featuresFromGeocodeResults = data.features; // }) @@ -653,7 +643,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { layers: ['map-routes-layer'], }); - console.error(features); + this.Document._childFilters = new List<string>(StrListCast(this.Document._childFilters).filter(filter => !filter.includes(LinkedTo))); if (features && features.length > 0 && features[0].properties && features[0].geometry) { const { routeTitle } = features[0].properties; const routeDoc: Doc | undefined = this.allRoutes.find(rtDoc => rtDoc.title === routeTitle); @@ -668,9 +658,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; - MapAnchorMenu.Instance.Reset(); - MapAnchorMenu.Instance.setRouteDoc(routeDoc); // TODO: Subject to change @@ -833,10 +821,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @computed get preAnimationViewState() { - if (!this._isAnimating) { - return this.mapboxMapViewState; - } - return undefined; + return !this._isAnimating ? this.mapboxMapViewState : undefined; } @action @@ -846,24 +831,15 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action updateAnimationSpeed = () => { - let newAnimationSpeed: AnimationSpeed; + this._animationSpeed = (() => { switch (this._animationSpeed) { - case AnimationSpeed.SLOW: - newAnimationSpeed = AnimationSpeed.MEDIUM; - break; - case AnimationSpeed.MEDIUM: - newAnimationSpeed = AnimationSpeed.FAST; - break; - case AnimationSpeed.FAST: - newAnimationSpeed = AnimationSpeed.SLOW; - break; - default: - newAnimationSpeed = AnimationSpeed.MEDIUM; - break; - } - this._animationSpeed = newAnimationSpeed; + case AnimationSpeed.SLOW: return AnimationSpeed.MEDIUM; + case AnimationSpeed.MEDIUM: return AnimationSpeed.FAST; + case AnimationSpeed.FAST: return AnimationSpeed.SLOW; + default: return AnimationSpeed.MEDIUM; + }})(); // prettier-ignore if (this._animationUtility) { - this._animationUtility.updateAnimationSpeed(newAnimationSpeed); + this._animationUtility.updateAnimationSpeed(this._animationSpeed); } }; @computed get animationSpeedTooltipText(): string { @@ -890,19 +866,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._animationUtility?.updateIsStreetViewAnimation(newVal); }; - getFeatureFromRouteDoc = (routeDoc: Doc): Feature<Geometry, GeoJsonProperties> => { - const geometry: LineString = { + getFeatureFromRouteDoc = (routeDoc: Doc): Feature<Geometry, GeoJsonProperties> => ({ + type: 'Feature', + properties: { + routeTitle: routeDoc.title, + }, + geometry: { type: 'LineString', coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)), - }; - return { - type: 'Feature', - properties: { - routeTitle: routeDoc.title, - }, - geometry: geometry, - }; - }; + }, + }); @action playAnimation = (status: AnimationStatus) => { @@ -936,7 +909,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const updateAnimationPhase = (newAnimationPhase: number) => this.setAnimationPhase(newAnimationPhase); if (status !== AnimationStatus.RESUME) { - const result = await animationUtil.flyInAndRotate({ + await animationUtil.flyInAndRotate({ map: this._mapRef.current!, // targetLngLat, // duration 3000 @@ -948,9 +921,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // endPitch: this.isStreetViewAnimation ? 80 : 50, updateFrameId, }); - - console.log('Bearing: ', result.bearing); - console.log('Altitude: ', result.altitude); } runInAction(() => { @@ -1028,7 +998,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.playAnimation(AnimationStatus.START); // Play from the beginning } }} - icon={this._isAnimating && this._finishedFlyTo ? <FontAwesomeIcon icon={faPause as IconLookup} /> : <FontAwesomeIcon icon={faPlay as IconLookup} />} + icon={<FontAwesomeIcon icon={this._isAnimating && this._finishedFlyTo ? faPause : faPlay} />} color="black" size={Size.MEDIUM} /> @@ -1039,12 +1009,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.stopAnimation(false); this.playAnimation(AnimationStatus.START); }} - icon={<FontAwesomeIcon icon={faRotate as IconLookup} />} + icon={<FontAwesomeIcon icon={faRotate} />} color="black" size={Size.MEDIUM} /> )} - <IconButton style={{ marginRight: '10px' }} tooltip="Stop and close animation" onPointerDown={() => this.stopAnimation(true)} icon={<FontAwesomeIcon icon={faCircleXmark as IconLookup} />} color="black" size={Size.MEDIUM} /> + <IconButton style={{ marginRight: '10px' }} tooltip="Stop and close animation" onPointerDown={() => this.stopAnimation(true)} icon={<FontAwesomeIcon icon={faCircleXmark} />} color="black" size={Size.MEDIUM} /> <div className="animation-suboptions"> <div>|</div> <FormControlLabel className="first-person-label" label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this._isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} /> @@ -1085,7 +1055,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { onBearingChange = (e: React.ChangeEvent<HTMLInputElement>) => { const bearing = parseInt(e.target.value); if (!isNaN(bearing) && this._mapRef.current) { - console.log('bearing change'); const fixedBearing = Math.max(0, Math.min(360, bearing)); this._mapRef.current.setBearing(fixedBearing); this.dataDoc.map_bearing = fixedBearing; @@ -1096,7 +1065,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { onPitchChange = (e: React.ChangeEvent<HTMLInputElement>) => { const pitch = parseInt(e.target.value); if (!isNaN(pitch) && this._mapRef.current) { - console.log('pitch change'); const fixedPitch = Math.max(0, Math.min(85, pitch)); this._mapRef.current.setPitch(fixedPitch); this.dataDoc.map_pitch = fixedPitch; @@ -1141,16 +1109,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._showTerrain = !this._showTerrain; }; - getMarkerIcon = (pinDoc: Doc): JSX.Element | null => { - const markerType = StrCast(pinDoc.markerType); - const markerColor = StrCast(pinDoc.markerColor); - - return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor) ?? null; - }; + getMarkerIcon = (pinDoc: Doc) => MarkerIcons.getFontAwesomeIcon(StrCast(pinDoc.markerType), '2x', StrCast(pinDoc.markerColor)) ?? null; render() { - const scale = this._props.NativeDimScaling?.() || 1; - const parscale = scale === 1 ? 1 : (this.ScreenToLocalBoxXf().Scale ?? 1); + TraceMobx(); + const scale = (this._props.NativeDimScaling?.() || 1) + 0.001; // bcz: weird, but without this hack, MapBox doesn't locate map correctly + const parscale = this.ScreenToLocalBoxXf().Scale; return ( <div className="mapBox" ref={this._ref}> @@ -1158,13 +1122,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { className="mapBox-wrapper" onWheel={e => e.stopPropagation()} onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} - style={{ transformOrigin: 'top left', transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> + style={{ transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> {!this._routeToAnimate && ( - <div className="mapBox-searchbar" style={{ width: `${100 / scale}%`, zIndex: 1, position: 'relative', background: 'lightGray' }}> + <div className="mapBox-searchbar" style={{ width: `${100 / scale}%` }}> <TextField fullWidth placeholder="Enter a location" onKeyDown={this.searchbarKeyDown} onChange={e => this.handleSearchChange(e.target.value)} /> - <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={() => this.toggleSettings()} /> + <IconButton icon={<FontAwesomeIcon icon={faGear} size="1x" />} type={Type.TERT} onClick={this.toggleSettings} /> <div style={{ opacity: 0 }}> - <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={() => this.toggleSettings()} /> + <IconButton icon={<FontAwesomeIcon icon={faGear} size="1x" />} type={Type.TERT} onClick={this.toggleSettings} /> </div> </div> )} @@ -1188,15 +1152,15 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { </div> <div className="mapbox-bearing-selection"> <div>Bearing: </div> - <input value={NumCast(this.mapboxMapViewState.bearing).toFixed(0)} type="number" onChange={this.onBearingChange} /> + <input value={this.mapboxMapViewState.bearing.toFixed(0)} type="number" onChange={this.onBearingChange} /> </div> <div className="mapbox-pitch-selection"> <div>Pitch: </div> - <input value={NumCast(this.mapboxMapViewState.pitch).toFixed(0)} type="number" onChange={this.onPitchChange} /> + <input value={this.mapboxMapViewState.pitch.toFixed(0)} type="number" onChange={this.onPitchChange} /> </div> <div className="mapbox-pitch-selection"> <div>Zoom: </div> - <input value={NumCast(this.mapboxMapViewState.zoom).toFixed(0)} type="number" onChange={this.onZoomChange} /> + <input value={this.mapboxMapViewState.zoom.toFixed(0)} type="number" onChange={this.onZoomChange} /> </div> <div className="mapbox-terrain-selection"> <div>Show terrain: </div> @@ -1230,17 +1194,18 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { )} <MapProvider> <MapboxMap + key={'' + this.Document.x + this.Document.y} // force map to rerender after dragging, otherwise it will display the wrong location until it gets re-rendered ref={this._mapRef} mapboxAccessToken={MAPBOX_ACCESS_TOKEN} - viewState={this._isAnimating || this._routeToAnimate ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }} + viewState={this._isAnimating || this._routeToAnimate ? undefined : { ...this.mapboxMapViewState, width: this._props.PanelWidth(), height: this._props.PanelHeight() }} mapStyle={this.dataDoc.map_style ? StrCast(this.dataDoc.map_style) : 'mapbox://styles/mapbox/streets-v11'} style={{ position: 'absolute', top: 0, left: 0, zIndex: '0', - width: NumCast(this.layoutDoc._width) * parscale, - height: NumCast(this.layoutDoc._height) * parscale, + width: this._props.PanelWidth() * parscale, + height: this._props.PanelHeight() * parscale, }} initialViewState={this._isAnimating ? undefined : this.mapboxMapViewState} onZoom={this.onMapZoom} @@ -1315,19 +1280,18 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { /> </> )} - - {!this._isAnimating && - this._animationPhase === 0 && - this.allPushpins // .filter(anno => !anno.layout_unrendered) - .map((pushpin, idx) => ( - <Marker key={idx} longitude={NumCast(pushpin.longitude)} latitude={NumCast(pushpin.latitude)} anchor="bottom" onClick={e => this.handleMarkerClick(e.originalEvent.clientX, e.originalEvent.clientY, pushpin)}> - {this.getMarkerIcon(pushpin)} - </Marker> - ))} - - {/* {this.mapMarkers.length > 0 && this.mapMarkers.map((marker, idx) => ( - <Marker key={idx} longitude={marker.longitude} latitude={marker.latitude}/> - ))} */} + {this._isAnimating || this._animationPhase + ? null + : this.allPushpins.map(p => ( + <Marker + key={'' + p.longitude + p.latitude} + longitude={NumCast(p.longitude)} + latitude={NumCast(p.latitude)} + anchor="bottom" + onClick={e => this.handleMarkerClick(e.originalEvent.clientX, e.originalEvent.clientY, p)}> + {this.getMarkerIcon(p)} + </Marker> + ))} </MapboxMap> </MapProvider> </div> @@ -1336,7 +1300,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ref={this._sidebarRef} {...this._props} fieldKey={this.fieldKey} - Document={this.Document} + Doc={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} usePanelWidth diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx index 0627d382e..e0efab576 100644 --- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx +++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx @@ -4,7 +4,7 @@ import { Button, EditableText, IconButton, Type } from '@dash/components'; import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { MapProvider, Map as MapboxMap } from 'react-map-gl'; +import { MapProvider, Map as MapboxMap } from 'react-map-gl/mapbox'; import { ClientUtils, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; import { Doc, DocListCast, Field, LinkedTo, Opt, returnEmptyDoclist } from '../../../../fields/Doc'; @@ -798,7 +798,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> PanelHeight={returnOne} NativeWidth={returnOne} NativeHeight={returnOne} - onKey={undefined} onDoubleClickScript={undefined} childFilters={returnEmptyFilter} childFiltersByRanges={returnEmptyFilter} @@ -830,7 +829,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> ref={this._sidebarRef} {...this._props} fieldKey={this.fieldKey} - Document={this.Document} + Doc={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} usePanelWidth diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index f2160feb7..eaea272dc 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -23,9 +23,9 @@ // glr: This should really be the same component as text and PDFs .pdfBox-sidebarBtn { background: global.$black; - height: 25px; - width: 25px; - right: 5px; + height: 20px; + width: 20px; + right: 0px; color: global.$white; display: flex; position: absolute; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 06b75e243..83c44c80f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -129,15 +129,14 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { cropping._width = anchw; cropping._height = anchh; cropping.onClick = undefined; - const croppingProto = cropping[DocData]; - croppingProto.annotationOn = undefined; - croppingProto.isDataDoc = true; - croppingProto.proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO - croppingProto.type = DocumentType.IMG; - croppingProto.layout = ImageBox.LayoutString('data'); - croppingProto.data = new ImageField(ClientUtils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')); - croppingProto.data_nativeWidth = anchw; - croppingProto.data_nativeHeight = anchh; + cropping.$annotationOn = undefined; + cropping.$isDataDoc = true; + cropping.$proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO + cropping.$type = DocumentType.IMG; + cropping.$layout = ImageBox.LayoutString('data'); + cropping.$data = new ImageField(ClientUtils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')); + cropping.$data_nativeWidth = anchw; + cropping.$data_nativeHeight = anchh; if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); } @@ -157,7 +156,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ClientUtils.convertDataUri(dataUrl, region[Id]).then(returnedfilename => setTimeout( action(() => { - croppingProto.data = new ImageField(returnedfilename); + cropping.$data = new ImageField(returnedfilename); }), 500 ) @@ -211,7 +210,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this._disposers.scroll = reaction( () => this.layoutDoc.layout_scrollTop, () => { - if (!(ComputedField.WithoutComputed(() => FieldValue(this.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { + if (!(ComputedField.DisableCompute(() => FieldValue(this.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { this.Document[this.SidebarKey + '_panY'] = ComputedField.MakeFunction('this.layout_scrollTop'); } this.layoutDoc[this.SidebarKey + '_freeform_scale'] = 1; @@ -500,8 +499,10 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { title="Toggle Sidebar" style={{ display: !this._props.isContentActive() ? 'none' : undefined, - top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, + top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 0, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, + transformOrigin: 'top right', + transform: `scale(${this._props.DocumentView?.().UIBtnScaling || 1})`, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> @@ -536,7 +537,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <SidebarAnnos ref={this._sidebarRef} {...this._props} - Document={this.Document} + Doc={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} setHeight={emptyFunction} @@ -607,6 +608,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { pdfBox={this} sidebarAddDoc={this.sidebarAddDocument} addDocTab={this.sidebarAddDocTab} + Doc={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} pdf={this._pdf} diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 7ba313e92..25c708da8 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -3,7 +3,6 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast } from '../../../../fields/Doc'; -import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { BoolCast, DocCast } from '../../../../fields/Types'; @@ -99,7 +98,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { }); screengrabber.overlayX = 70; // was -400 screengrabber.overlayY = 590; // was 0 - screengrabber[DocData][Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true; + screengrabber['$' + Doc.LayoutDataKey(screengrabber) + '_trackScreen'] = true; Doc.AddToMyOverlay(screengrabber); // just adds doc to overlay DocumentView.addViewRenderedCb(screengrabber, docView => { RecordingBox.screengrabber = docView.ComponentView as RecordingBox; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 6289470b6..4f02d68d6 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -284,7 +284,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() setupDictation = () => { if (this.dataDoc[this.fieldKey + '_dictation']) return; const dictationText = DocUtils.GetNewTextDoc('dictation', NumCast(this.Document.x), NumCast(this.Document.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); - const textField = Doc.LayoutFieldKey(dictationText); + const textField = Doc.LayoutDataKey(dictationText); dictationText._layout_autoHeight = false; const dictationTextProto = dictationText[DocData]; dictationTextProto[`${textField}_recordingSource`] = this.dataDoc; @@ -301,7 +301,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <div className="videoBox-viewer"> <div style={{ position: 'relative', height: this.videoPanelHeight() }}> <CollectionFreeFormView - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} setContentViewBox={emptyFunction} NativeWidth={returnZero} @@ -329,7 +328,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <div style={{ background: SettingsManager.userColor, position: 'relative', height: this.formattedPanelHeight() }}> {!(this.dataDoc[this.fieldKey + '_dictation'] instanceof Doc) ? null : ( <FormattedTextBox - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} Document={DocCast(this.dataDoc[this.fieldKey + '_dictation'])} fieldKey="text" diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 8da422039..38a43e8d4 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -1,14 +1,12 @@ -/* eslint-disable react/button-has-type */ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { returnAlways, returnEmptyString } from '../../../ClientUtils'; -import { Doc } from '../../../fields/Doc'; +import { Doc, StrListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; -import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -26,10 +24,8 @@ import './ScriptingBox.scss'; import * as ts from 'typescript'; import { FieldType } from '../../../fields/ObjectField'; -// eslint-disable-next-line @typescript-eslint/no-var-requires const getCaretCoordinates = require('textarea-caret'); -// eslint-disable-next-line @typescript-eslint/no-var-requires const ReactTextareaAutocomplete = require('@webscopeio/react-textarea-autocomplete').default; @observer @@ -105,7 +101,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this.dataDoc[this.fieldKey + '-functionDescription'] = value; } @computed({ keepAlive: true }) get compileParams() { - return Cast(this.dataDoc[this.fieldKey + '-params'], listSpec('string'), []); + return StrListCast(this.dataDoc[this.fieldKey + '-params']); } set compileParams(value) { this.dataDoc[this.fieldKey + '-params'] = new List<string>(value); @@ -426,7 +422,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() onChange={e => this.viewChanged(e, parameter)} value={typeof this.dataDoc[parameter] === 'string' ? 'S' + StrCast(this.dataDoc[parameter]) : typeof this.dataDoc[parameter] === 'number' ? 'N' + NumCast(this.dataDoc[parameter]) : 'B' + BoolCast(this.dataDoc[parameter])}> {types.map((type, i) => ( - // eslint-disable-next-line react/no-array-index-key <option key={i} className="scriptingBox-viewOption" value={(typeof type === 'string' ? 'S' : typeof type === 'number' ? 'N' : 'B') + type}> {' '} {type.toString()}{' '} @@ -680,7 +675,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const definedParameters = !this.compileParams.length ? null : ( <div className="scriptingBox-plist" style={{ width: '30%' }}> {this.compileParams.map((parameter, i) => ( - // eslint-disable-next-line react/no-array-index-key <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}> <EditableView display="block" @@ -760,7 +754,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {!this.compileParams.length || !this.paramsNames ? null : ( <div className="scriptingBox-plist"> {this.paramsNames.map((parameter: string, i: number) => ( - // eslint-disable-next-line react/no-array-index-key <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}> <div className="scriptingBox-wrapper" style={{ maxHeight: '40px' }}> <div className="scriptingBox-paramNames"> {`${parameter}:${this.paramsTypes[i]} = `} </div> diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 9adee53e8..fa099178c 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -331,7 +331,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc)); this._props.addDocument?.(imageSnapshot); DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); - // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); // do we need to set an end time? should default to +0.1 + // link && (DocCast(link.link_anchor_2).$timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); // do we need to set an end time? should default to +0.1 setTimeout(() => downX !== undefined && downY !== undefined && DocumentView.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true)); }; @@ -918,11 +918,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { crop = (region: Doc | undefined, addCrop?: boolean) => { if (!region) return undefined; const cropping = Doc.MakeCopy(region, true); - const regionData = region[DocData]; - regionData.backgroundColor = 'transparent'; - regionData.lockedPosition = true; - regionData.title = 'region:' + this.Document.title; - regionData.followLinkToggle = true; + region.$backgroundColor = 'transparent'; + region.$lockedPosition = true; + region.$title = 'region:' + this.Document.title; + region.$followLinkToggle = true; region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001; this.addDocument(region); const anchx = NumCast(cropping.x); @@ -938,25 +937,24 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { cropping.timecodeToHide = undefined; cropping.timecodeToShow = undefined; cropping.onClick = undefined; - const croppingProto = cropping[DocData]; - croppingProto.annotationOn = undefined; - croppingProto.isDataDoc = true; - croppingProto.proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO - croppingProto.type = DocumentType.VID; - croppingProto.layout = VideoBox.LayoutString('data'); - croppingProto.data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField); - croppingProto.data_nativeWidth = anchw; - croppingProto.data_nativeHeight = anchh; - croppingProto.videoCrop = true; - croppingProto.layout_currentTimecode = this.layoutDoc._layout_currentTimecode; - croppingProto.freeform_scale = viewScale; - croppingProto.freeform_scale_min = viewScale; - croppingProto.freeform_ = anchx / viewScale; - croppingProto.freeform_panY = anchy / viewScale; - croppingProto.freeform_panX_min = anchx / viewScale; - croppingProto.freeform_panX_max = anchw / viewScale; - croppingProto.freeform_panY_min = anchy / viewScale; - croppingProto.freeform_panY_max = anchh / viewScale; + cropping.$annotationOn = undefined; + cropping.$isDataDoc = true; + cropping.$proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO + cropping.$type = DocumentType.VID; + cropping.$layout = VideoBox.LayoutString('data'); + cropping.$data = ObjectField.MakeCopy(this.dataDoc[this.fieldKey] as ObjectField); + cropping.$data_nativeWidth = anchw; + cropping.$data_nativeHeight = anchh; + cropping.$videoCrop = true; + cropping.$layout_currentTimecode = this.layoutDoc._layout_currentTimecode; + cropping.$freeform_scale = viewScale; + cropping.$freeform_scale_min = viewScale; + cropping.$freeform_ = anchx / viewScale; + cropping.$freeform_panY = anchy / viewScale; + cropping.$freeform_panX_min = anchx / viewScale; + cropping.$freeform_panX_max = anchw / viewScale; + cropping.$freeform_panY_min = anchy / viewScale; + cropping.$freeform_panY_max = anchh / viewScale; if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index e7a10cc29..5603786f0 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -6,7 +6,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import * as WebRequest from 'web-request'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivHeight, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils'; -import { Doc, DocListCast, Field, FieldType, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { HtmlField } from '../../../fields/HtmlField'; import { InkTool } from '../../../fields/InkField'; @@ -54,7 +54,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } public static openSidebarWidth = 250; public static sidebarResizerWidth = 5; - static webStyleSheet = addStyleSheet(); + static webStyleSheet = addStyleSheet().sheet; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _outerRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -645,8 +645,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; forward = (checkAvailable?: boolean) => { - const future = Cast(this.dataDoc[this.fieldKey + '_future'], listSpec('string'), []); - const history = Cast(this.dataDoc[this.fieldKey + '_history'], listSpec('string'), []); + const future = StrListCast(this.dataDoc[this.fieldKey + '_future']); + const history = StrListCast(this.dataDoc[this.fieldKey + '_history']); if (checkAvailable) return future.length; runInAction(() => { if (future.length) { @@ -672,23 +672,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; back = (checkAvailable?: boolean) => { - const future = Cast(this.dataDoc[this.fieldKey + '_future'], listSpec('string')); - const history = Cast(this.dataDoc[this.fieldKey + '_history'], listSpec('string'), []); + const future = StrListCast(this.dataDoc[this.fieldKey + '_future']); + const history = StrListCast(this.dataDoc[this.fieldKey + '_history']); if (checkAvailable) return history.length; runInAction(() => { if (history.length) { const curUrl = this._url; - if (future === undefined) this.dataDoc[this.fieldKey + '_future'] = new List<string>([this._url]); + if (!future.length) this.dataDoc[this.fieldKey + '_future'] = new List<string>([this._url]); else this.dataDoc[this.fieldKey + '_future'] = new List<string>([...future, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; - setTimeout( - action(() => { - this._webUrl = this._url; - }) - ); + setTimeout(action(() => (this._webUrl = this._url))); } else { this._webUrl = this._url; } @@ -1220,7 +1216,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { {...this._props} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} fieldKey={this.fieldKey + '_' + this._urlHash} - Document={this.Document} + Doc={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} setHeight={emptyFunction} diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx index 009eb82cd..6f1f58a4c 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.tsx +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -1,23 +1,22 @@ import { Calendar, EventClickArg, EventDropArg, EventSourceInput } from '@fullcalendar/core'; import dayGridPlugin from '@fullcalendar/daygrid'; +import interactionPlugin from '@fullcalendar/interaction'; import multiMonthPlugin from '@fullcalendar/multimonth'; import timeGrid from '@fullcalendar/timegrid'; -import interactionPlugin from '@fullcalendar/interaction'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { dateRangeStrToDates } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; -import { BoolCast, NumCast, StrCast } from '../../../../fields/Types'; -import { CollectionSubView, SubCollectionViewProps } from '../../collections/CollectionSubView'; -import './CalendarBox.scss'; import { Id } from '../../../../fields/FieldSymbols'; +import { BoolCast, NumCast, StrCast } from '../../../../fields/Types'; import { DocServer } from '../../../DocServer'; -import { DocumentView } from '../DocumentView'; -import { OpenWhere } from '../OpenWhere'; import { DragManager } from '../../../util/DragManager'; -import { DocData } from '../../../../fields/DocSymbols'; +import { CollectionSubView, SubCollectionViewProps } from '../../collections/CollectionSubView'; import { ContextMenu } from '../../ContextMenu'; +import { DocumentView } from '../DocumentView'; +import { OpenWhere } from '../OpenWhere'; +import './CalendarBox.scss'; type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay'; @@ -113,7 +112,7 @@ export class CalendarBox extends CollectionSubView() { if (!super.onInternalDrop(e, de)) return false; de.complete.docDragData?.droppedDocuments.forEach(doc => { const today = new Date().toISOString(); - if (!doc.date_range) doc[DocData].date_range = `${today}|${today}`; + if (!doc.date_range) doc.$date_range = `${today}|${today}`; }); return true; }; diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index 6e9307d37..528bcd05a 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -733,9 +733,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const x2 = parseFloat(values[2]) * Doc.NativeWidth(doc); const y2 = parseFloat(values[3]) * Doc.NativeHeight(doc) + foundChunk.startPage * Doc.NativeHeight(doc); - const annotationKey = Doc.LayoutFieldKey(doc) + '_annotations'; + const annotationKey = '$' + Doc.LayoutDataKey(doc) + '_annotations'; - const existingDoc = DocListCast(doc[DocData][annotationKey]).find(d => d.citation_id === citation.citation_id); + const existingDoc = DocListCast(doc[annotationKey]).find(d => d.citation_id === citation.citation_id); const highlightDoc = existingDoc ?? this.createImageCitationHighlight(x1, y1, x2, y2, citation, annotationKey, doc); DocumentManager.Instance.showDocument(highlightDoc, { willZoomCentered: true }, () => {}); @@ -779,7 +779,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { _height: y2 - y1, backgroundColor: 'rgba(255, 255, 0, 0.5)', }); - highlight_doc[DocData].citation_id = citation.citation_id; + highlight_doc.$citation_id = citation.citation_id; Doc.AddDocToList(pdfDoc[DocData], annotationKey, highlight_doc); highlight_doc.annotationOn = pdfDoc; Doc.SetContainer(highlight_doc, pdfDoc); diff --git a/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts b/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts index afd34f28d..c1915a398 100644 --- a/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts +++ b/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts @@ -42,7 +42,8 @@ export class Vectorstore { constructor(id: string, doc_ids: () => string[]) { const pineconeApiKey = process.env.PINECONE_API_KEY; if (!pineconeApiKey) { - throw new Error('PINECONE_API_KEY is not defined.'); + console.log('PINECONE_API_KEY is not defined - Vectorstore will be unavailable'); + return; } // Initialize Pinecone and OpenAI clients with API keys from the environment. diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx index ec1f7a023..797a9b812 100644 --- a/src/client/views/nodes/formattedText/DailyJournal.tsx +++ b/src/client/views/nodes/formattedText/DailyJournal.tsx @@ -1,14 +1,22 @@ -import { action, makeObservable, observable } from 'mobx'; +import { makeObservable, action, observable } from 'mobx'; import * as React from 'react'; -import { RichTextField } from '../../../../fields/RichTextField'; import { Docs } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from '../FieldView'; import { FormattedTextBox, FormattedTextBoxProps } from './FormattedTextBox'; +import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; +import { RichTextField } from '../../../../fields/RichTextField'; +import { Plugin } from 'prosemirror-state'; +import { RTFCast } from '../../../../fields/Types'; export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() { @observable journalDate: string; + @observable typingTimeout: NodeJS.Timeout | null = null; // Track typing delay + @observable lastUserText: string = ''; // Store last user-entered text + _ref = React.createRef<FormattedTextBox>(); // reference to the formatted textbox + predictiveTextRange: { from: number; to: number } | null = null; // where predictive text starts and ends + private predictiveText: string | null = ' ... why?'; public static LayoutString(fieldStr: string) { return FieldView.LayoutString(DailyJournal, fieldStr); @@ -18,12 +26,13 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() super(props); makeObservable(this); this.journalDate = this.getFormattedDate(); - - console.log('Constructor: Setting initial title and text...'); - this.setDailyTitle(); - this.setDailyText(); } + /** + * Method to get the current date in standard format + * @returns - date in standard long format + */ + getFormattedDate(): string { const date = new Date().toLocaleDateString(undefined, { weekday: 'long', @@ -35,6 +44,9 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() return date; } + /** + * Method to set the title of the node to the date + */ @action setDailyTitle() { console.log('setDailyTitle() called...'); @@ -48,43 +60,259 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() console.log('New title after update:', this.dataDoc.title); } + /** + * Method to set the standard text of the node (to the current date) + */ @action setDailyText() { - console.log('setDailyText() called...'); const placeholderText = 'Start writing here...'; - const initialText = `Journal Entry - ${this.journalDate}\n${placeholderText}`; + const dateText = `${this.journalDate}\n`; console.log('Checking if dataDoc has text field...'); - const styles = { - bold: true, // Make the journal date bold - color: 'blue', // Set the journal date color to blue - fontSize: 18, // Set the font size to 18px for the whole text - }; - - console.log('Setting new text field with:', initialText); - this.dataDoc[this.fieldKey] = RichTextField.textToRtf( - initialText, - undefined, // No image DocId - styles, // Pass the styles object here - placeholderText.length // The position for text selection + this.dataDoc[this.fieldKey] = RichTextField.textToRtfFormat( + [ + { text: 'Journal Entry:', styles: { bold: true, color: 'black', fontSize: 20 } }, + { text: dateText, styles: { italic: true, color: 'gray', fontSize: 15 } }, + { text: placeholderText, styles: { fontSize: 14, color: 'gray' } }, + ], + undefined, + placeholderText.length ); console.log('Current text field:', this.dataDoc[this.fieldKey]); } + /** + * Tracks user typing text inout into the node, to call the insert predicted + * text function when appropriate (i.e. when the user stops typing) + */ + + @action onTextInput = () => { + const editorView = this._ref.current?.EditorView; + if (!editorView) return; + + if (this.typingTimeout) clearTimeout(this.typingTimeout); + + this.typingTimeout = setTimeout(() => { + this.insertPredictiveQuestion(); + }, 3500); + }; + + /** + * Inserts predictive text at the end of what the user is typing + */ + + @action insertPredictiveQuestion = async () => { + const editorView = this._ref.current?.EditorView; + if (!editorView) return; + + const { state, dispatch } = editorView; + const { schema } = state; + const { to } = state.selection; + const insertPos = to; // cursor position + + const resolvedPos = state.doc.resolve(insertPos); + const parentNode = resolvedPos.parent; + const indexInParent = resolvedPos.index(); + const isAtEndOfParent = indexInParent >= parentNode.childCount; + + // Check if there's a line break or paragraph node after the current position + let hasNewlineAfter = false; + try { + const nextNode = parentNode.child(indexInParent); + hasNewlineAfter = nextNode.type.name === schema.nodes.hard_break.name || nextNode.type.name === schema.nodes.paragraph.name; + } catch { + hasNewlineAfter = false; + } + + // Only insert if we're at end of node, or there's a newline node after + if (!isAtEndOfParent && !hasNewlineAfter) return; + + const fontSizeMark = schema.marks.pFontSize.create({ fontSize: '14px' }); + const fontColorMark = schema.marks.pFontColor.create({ fontColor: 'lightgray' }); + const fontItalicsMark = schema.marks.em.create(); + + this.predictiveText = ' ...'; // placeholder for now + + const fullTextUpToCursor = state.doc.textBetween(0, state.selection.to, '\n', '\n'); + const gptPrompt = `Given the following incomplete journal entry, generate a single 2-5 word question that continues the user's thought:\n\n"${fullTextUpToCursor}"`; + const res = await gptAPICall(gptPrompt, GPTCallType.COMPLETION); + if (!res) return; + + // styled text node + const text = ` ... ${res.trim()}`; + const predictedText = schema.text(text, [fontSizeMark, fontColorMark, fontItalicsMark]); + + // Insert styled text at cursor position + const transaction = state.tr.insert(insertPos, predictedText).setStoredMarks([state.schema.marks.pFontColor.create({ fontColor: 'gray' })]); // should probably instead inquire marks before predictive prompt + dispatch(transaction); + + this.predictiveText = text; + }; + + createPredictiveCleanupPlugin = () => { + return new Plugin({ + view: () => { + return { + update: (view, prevState) => { + const { state, dispatch } = view; + if (!this.predictiveText) return; + + // Check if doc or selection changed + if (!prevState.doc.eq(state.doc) || !prevState.selection.eq(state.selection)) { + let found = false; + const textToRemove = this.predictiveText; + + state.doc.descendants((node, pos) => { + if (node.isText && node.text === textToRemove) { + const tr = state.tr.delete(pos, pos + node.nodeSize); + + // Set the desired default marks for future input + const fontSizeMark = state.schema.marks.pFontSize.create({ fontSize: '14px' }); + const fontColorMark = state.schema.marks.pFontColor.create({ fontColor: 'gray' }); + tr.setStoredMarks([]); + tr.setStoredMarks([fontSizeMark, fontColorMark]); + + dispatch(tr); + + this.predictiveText = null; + return false; + } + return true; + }); + + if (!found) { + // fallback cleanup + this.predictiveText = null; + } + } + }, + }; + }, + }); + }; + componentDidMount(): void { console.log('componentDidMount() triggered...'); - // bcz: This should be moved into Docs.Create.DailyJournalDocument() - // otherwise, it will override all the text whenever the note is reloaded - this.setDailyTitle(); - this.setDailyText(); + console.log('Text: ' + RTFCast(this.Document.text)?.Text); + + const editorView = this._ref.current?.EditorView; + if (editorView) { + editorView.dom.addEventListener('input', this.onTextInput); + + // Add plugin to state if not already added + const cleanupPlugin = this.createPredictiveCleanupPlugin(); + this._ref.current?.addPlugin(cleanupPlugin); + } + + const rawText = RTFCast(this.Document.text)?.Text ?? ''; + const isTextEmpty = !rawText || rawText === ''; + + const currentTitle = this.dataDoc.title || ''; + const isTitleString = typeof currentTitle === 'string'; + const isDefaultTitle = isTitleString && currentTitle.includes('Untitled DailyJournal'); + + if (isTextEmpty && isDefaultTitle) { + console.log('Journal title and text are default. Initializing...'); + this.setDailyTitle(); + this.setDailyText(); + } else { + console.log('Journal already has content. Skipping initialization.'); + } } + componentWillUnmount(): void { + const editorView = this._ref.current?.EditorView; + if (editorView) { + editorView.dom.removeEventListener('input', this.onTextInput); + } + if (this.typingTimeout) clearTimeout(this.typingTimeout); + } + + @action handleGeneratePrompts = async () => { + const rawText = RTFCast(this.Document.text)?.Text ?? ''; + console.log('Extracted Journal Text:', rawText); + console.log('Before Update:', this.Document.text, 'Type:', typeof this.Document.text); + + if (!rawText.trim()) { + alert('Journal is empty! Write something first.'); + return; + } + + try { + // Call GPT API to generate prompts + const res = await gptAPICall('Generate 1-2 short journal prompts for the following journal entry: ' + rawText, GPTCallType.COMPLETION); + + if (!res) { + console.error('GPT call failed.'); + return; + } + + const editorView = this._ref.current?.EditorView; + if (!editorView) { + console.error('EditorView is not available.'); + return; + } else { + const { state, dispatch } = editorView; + const { schema } = state; + + // Use available marks + const boldMark = schema.marks.strong.create(); + const italicMark = schema.marks.em.create(); + const fontSizeMark = schema.marks.pFontSize.create({ fontSize: '14px' }); + const fontColorMark = schema.marks.pFontColor.create({ fontColor: 'gray' }); + + // Create text nodes with formatting + const headerText = schema.text('\n\n# Suggested Prompts:\n', [boldMark, italicMark, fontSizeMark, fontColorMark]); + const responseText = schema.text(res, [fontSizeMark, fontColorMark]); + + // Insert formatted text + const transaction = state.tr.insert(state.selection.from, headerText).insert(state.selection.from + headerText.nodeSize, responseText); + dispatch(transaction); + } + } catch (err) { + console.error('Error calling GPT:', err); + } + }; + render() { return ( - <div style={{ background: 'beige', width: '100%', height: '100%' }}> - <FormattedTextBox {...this._props} fieldKey={'text'} Document={this.Document} TemplateDataDocument={undefined} /> + <div + style={{ + // background: 'beige', + width: '100%', + height: '100%', + backgroundColor: 'beige', + backgroundImage: ` + repeating-linear-gradient( + to bottom, + rgba(255, 26, 26, 0.2) 0px, rgba(255, 26, 26, 0.2) 1px, /* Thin red stripes */ + transparent 1px, transparent 20px + ) + `, + backgroundSize: '100% 20px', + backgroundRepeat: 'repeat', + }}> + {/* GPT Button */} + <button + style={{ + position: 'absolute', + bottom: '5px', + right: '5px', + padding: '5px 10px', + backgroundColor: '#9EAD7C', + color: 'white', + border: 'none', + borderRadius: '5px', + cursor: 'pointer', + zIndex: 10, + }} + onClick={this.handleGeneratePrompts}> + Prompts + </button> + + <FormattedTextBox ref={this._ref} {...this._props} fieldKey={'text'} Document={this.Document} TemplateDataDocument={undefined} /> </div> ); } diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index 78bbb520e..3734ad9cc 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -3,7 +3,7 @@ .dashFieldView-active, .dashFieldView { position: relative; - display: inline-flex; + display: contents; align-items: center; .dashFieldView-enumerables { @@ -26,6 +26,7 @@ display: inline-block; font-weight: normal; background: rgba(0, 0, 0, 0.1); + align-content: center; cursor: default; } .dashFieldView-fieldSpan { @@ -33,12 +34,18 @@ margin-left: 2px; margin-right: 5px; padding-left: 2px; - display: inline-block; - background-color: rgba(155, 155, 155, 0.24); + font-size: smaller; + display: contents; + > div { + background-color: rgba(155, 155, 155, 0.24); + } span { user-select: all; min-width: 100%; display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; } } } diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index e899b49bc..7ea5d1fcf 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,8 +1,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; +import { Node } from 'prosemirror-model'; import { NodeSelection } from 'prosemirror-state'; +import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../../../ClientUtils'; @@ -13,6 +15,7 @@ import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; import { Cast, DocCast } from '../../../../fields/Types'; import { emptyFunction } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; +import { DocumentOptions, FInfo } from '../../../documents/Documents'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { Transform } from '../../../util/Transform'; import { undoable, undoBatch } from '../../../util/UndoManager'; @@ -23,9 +26,6 @@ import { ObservableReactComponent } from '../../ObservableReactComponent'; import { OpenWhere } from '../OpenWhere'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; -import { Node } from 'prosemirror-model'; -import { EditorView } from 'prosemirror-view'; -import { DocumentOptions, FInfo } from '../../../documents/Documents'; @observer export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -99,7 +99,6 @@ interface IDashFieldViewInternal { width: number; height: number; editable: boolean; - nodeSelected: () => boolean; node: Node; getPos: () => number; unclickable: () => boolean; @@ -112,7 +111,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi _fieldKey: string; _fieldRef = React.createRef<HTMLDivElement>(); @observable _dashDoc: Doc | undefined = undefined; - @observable _expanded = this._props.nodeSelected(); + @observable _expanded = false; constructor(props: IDashFieldViewInternal) { super(props); @@ -140,7 +139,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi componentWillUnmount() { this._reactionDisposer?.(); } - isRowActive = () => (this._props.nodeSelected() || this._expanded) && this._props.editable; + isRowActive = () => this._props.tbox._props.isContentActive() && this._props.editable; finishEdit = action(() => { if (this._expanded) { this._expanded = false; @@ -149,7 +148,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi setTimeout(() => !this._props.tbox.ProseRef?.contains(document.activeElement) && this._props.tbox._props.onBlur?.()); } }); - selectedCells = () => (this._dashDoc ? [this._dashDoc] : undefined); + selectedCells = () => (this._dashDoc && this._expanded ? [this._dashDoc] : undefined); columnWidth = () => Math.min(this._props.tbox._props.PanelWidth(), Math.max(50, this._props.tbox._props.PanelWidth() - 100)); // try to leave room for the fieldKey finfo = (fieldKey: string) => (new DocumentOptions() as Record<string, FInfo>)[fieldKey]; @@ -158,17 +157,18 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi @computed get fieldValueContent() { return !this._dashDoc ? null : ( <div + className="dashFieldView-fieldSpan" onPointerDown={action(() => { this._expanded = !this._props.editable ? false : !this._expanded; - })} - style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}> + })}> <SchemaTableCell - Document={this._dashDoc} + Doc={this._dashDoc} col={0} deselectCell={emptyFunction} - selectCell={emptyFunction} + selectCell={() => (this._expanded ? true : undefined)} + autoFocus={true} maxWidth={this._props.hideKey || this._hideKey ? undefined : this._props.tbox._props.PanelWidth} - columnWidth={this._expanded || this._props.nodeSelected() ? () => undefined : returnZero} + columnWidth={returnZero} selectedCells={this.selectedCells} selectedCol={returnZero} fieldKey={this._fieldKey} @@ -184,11 +184,10 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi getFinfo={this.finfo} setColumnValues={returnFalse} allowCRs - oneLine={!this._expanded && !this._props.nodeSelected()} + oneLine={!this._expanded && this._props.editable} finishEdit={this.finishEdit} transform={Transform.Identity} menuTarget={null} - autoFocus rootSelected={this._props.tbox._props.rootSelected} /> </div> @@ -233,7 +232,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi } @computed get _hideValue() { - return this._props.hideValue && !this._props.nodeSelected(); + return this._props.hideValue; } // clicking on the label creates a pivot view collection of all documents @@ -255,7 +254,6 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi }; @computed get values() { - if (this._props.nodeSelected()) return []; const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []); return vals.strings.map(facet => ({ value: facet, label: facet })); @@ -277,7 +275,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi </span> )} {this._props.fieldKey.startsWith('#') || this._hideValue ? null : this.fieldValueContent} - {!this.values.length ? null : ( + {!this.values.length || !this.props.editable ? null : ( <select className="dashFieldView-select" tabIndex={-1} defaultValue={this._dashDoc && Field.toKeyValueString(this._dashDoc, this._fieldKey)} onChange={this.selectVal}> <option value="-unset-">-unset-</option> {this.values.map(val => ( @@ -297,8 +295,6 @@ export class DashFieldView { node: Node; tbox: FormattedTextBox; getpos: () => number | undefined; - @observable _nodeSelected = false; - NodeSelected = () => this._nodeSelected; unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some(m => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); constructor(node: Node, view: EditorView, getPos: () => number | undefined, tbox: FormattedTextBox) { @@ -311,26 +307,14 @@ export class DashFieldView { this.dom.style.width = node.attrs.width; this.dom.style.height = node.attrs.height; this.dom.style.position = 'relative'; - this.dom.style.display = 'inline-block'; + this.dom.style.display = 'inline-flex'; + this.dom.style.maxWidth = '100%'; this.dom.onkeypress = function (e: KeyboardEvent) { e.stopPropagation(); }; - this.dom.onkeydown = (e: KeyboardEvent) => { + this.dom.onkeydown = action((e: KeyboardEvent) => { e.stopPropagation(); - if (e.key === 'Tab') { - e.preventDefault(); - const editor = tbox.EditorView; - if (editor) { - const { state } = editor; - for (let i = getPosition() + 1; i < state.doc.content.size; i++) { - if (state.doc.nodeAt(i)?.type.name === state.schema.nodes.dashField.name) { - editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(i)))); - return; - } - } - } - } - }; + }); this.dom.onkeyup = function (e: KeyboardEvent) { e.stopPropagation(); }; @@ -351,7 +335,6 @@ export class DashFieldView { hideKey={node.attrs.hideKey} hideValue={node.attrs.hideValue} editable={node.attrs.editable} - nodeSelected={this.NodeSelected} tbox={tbox} /> ); @@ -365,19 +348,6 @@ export class DashFieldView { } }); } - deselectNode() { - runInAction(() => { - this._nodeSelected = false; - }); - this.dom.classList.remove('ProseMirror-selectednode'); - } - selectNode() { - setTimeout( - action(() => { - this._nodeSelected = true; - }), - 100 - ); - this.dom.classList.add('ProseMirror-selectednode'); - } + deselectNode() {} + selectNode() {} } diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx index e0450b202..827db190a 100644 --- a/src/client/views/nodes/formattedText/EquationView.tsx +++ b/src/client/views/nodes/formattedText/EquationView.tsx @@ -6,7 +6,6 @@ import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; -import { DocData } from '../../../../fields/DocSymbols'; import { StrCast } from '../../../../fields/Types'; import './DashFieldView.scss'; import EquationEditor from './EquationEditor'; @@ -63,9 +62,9 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> }}> <EquationEditor ref={this._ref} - value={StrCast(this._textBoxDoc[DocData][this._fieldKey])} + value={StrCast(this._textBoxDoc['$' + this._fieldKey])} onChange={str => { - this._textBoxDoc[DocData][this._fieldKey] = str; + this._textBoxDoc['$' + this._fieldKey] = str; }} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index f9de4ab5a..547a2efa8 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -141,8 +141,8 @@ audiotag:hover { position: absolute; top: 0; right: 0; - width: 17px; - height: 17px; + width: 20px; + height: 20px; font-size: 11px; border-radius: 3px; color: white; @@ -153,6 +153,7 @@ audiotag:hover { align-items: center; cursor: grabbing; box-shadow: global.$standard-box-shadow; + transform-origin: top right; // transition: 0.2s; opacity: 0.3; &:hover { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 38817ac6d..dc23a695d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,10 +1,10 @@ /* eslint-disable no-use-before-define */ -import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; +import { Property } from 'csstype'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { baseKeymap, selectAll } from 'prosemirror-commands'; +import { baseKeymap, selectAll, splitBlock } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; import { inputRules } from 'prosemirror-inputrules'; import { keymap } from 'prosemirror-keymap'; @@ -13,20 +13,19 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti import { EditorView, NodeViewConstructor } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, imageUrlToBase64, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id, ToString } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from '../../../../fields/RichTextField'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; import { emptyFunction, numberRange, unimplementedFunction, Utils } from '../../../../Utils'; -import { gptAPICall, GPTCallType, gptImageLabel } from '../../../apis/gpt/GPT'; +import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; @@ -34,7 +33,6 @@ import { DocUtils } from '../../../documents/DocUtils'; import { DictationManager } from '../../../util/DictationManager'; import { DragManager } from '../../../util/DragManager'; import { dropActionType } from '../../../util/DropActionTypes'; -import { MakeTemplate } from '../../../util/DropConverter'; import { LinkManager } from '../../../util/LinkManager'; import { RTFMarkup } from '../../../util/RTFMarkup'; import { SnappingManager } from '../../../util/SnappingManager'; @@ -49,12 +47,14 @@ import { AnchorMenu } from '../../pdf/AnchorMenu'; import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; import { PinDocView, PinProps } from '../../PinFuncs'; import { SidebarAnnos } from '../../SidebarAnnos'; +import { StickerPalette } from '../../smartdraw/StickerPalette'; import { StyleProp } from '../../StyleProp'; import { styleFromLayoutString } from '../../StyleProvider'; import { mediaState } from '../AudioBox'; import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { FocusViewOptions } from '../FocusViewOptions'; +import { LabelBox } from '../LabelBox'; import { LinkInfo } from '../LinkDocPreview'; import { OpenWhere } from '../OpenWhere'; import './FormattedTextBox.scss'; @@ -64,9 +64,6 @@ import { removeMarkWithAttrs } from './prosemirrorPatches'; import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; -import { Property } from 'csstype'; -import { LabelBox } from '../LabelBox'; -import { StickerPalette } from '../../smartdraw/StickerPalette'; // import * as applyDevTools from 'prosemirror-dev-tools'; export interface FormattedTextBoxProps extends FieldViewProps { @@ -78,17 +75,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } - public static MakeConfig(rules?: RichTextRules, props?: FormattedTextBoxProps) { + public static MakeConfig(rules?: RichTextRules, textBox?: FormattedTextBox, plugs?: Plugin[]) { return { schema, plugins: [ inputRules(rules?.inpRules ?? { rules: [] }), - ...(props ? [FormattedTextBox.richTextMenuPlugin(props)] : []), + ...(textBox?._props ? [FormattedTextBox.richTextMenuPlugin(textBox._props)] : []), history(), - keymap(buildKeymap(schema, props ?? {})), + keymap(buildKeymap(schema, textBox)), keymap(baseKeymap), new Plugin({ props: { attributes: { class: 'ProseMirror-example-setup-style' } } }), new Plugin({ view: () => new FormattedTextBoxComment() }), + ...(plugs ?? []), ], }; } @@ -103,12 +101,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection private static _nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }; - private static _globalHighlightsCache: string = ''; - private static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']); - private static _highlightStyleSheet = addStyleSheet(); - private static _bulletStyleSheet = addStyleSheet(); - private static _userStyleSheet = addStyleSheet(); + private _curHighlights = new ObservableSet<string>(['Audio Tags']); + private static _highlightStyleSheet = addStyleSheet().sheet; + private static _bulletStyleSheet = addStyleSheet().sheet; + private _userStyleSheetElement: HTMLStyleElement | undefined; + private _enteringStyle = false; private _oldWheel: HTMLDivElement | null = null; private _selectionHTML: string | undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); @@ -140,30 +138,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB public ApplyingChange: string = ''; @observable _showSidebar = false; + @observable _userPlugins: Plugin[] = []; - @computed get fontColor() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore - @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore - @computed get fontFamily() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore - @computed get fontWeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore - @computed get fontStyle() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontStyle) as string; } // prettier-ignore - @computed get fontDecoration() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontDecoration) as string; } // prettier-ignore + @computed get fontColor() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore + @computed get fontSize() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore + @computed get fontFamily() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore + @computed get fontWeight() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore + @computed get fontStyle() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontStyle) as string; } // prettier-ignore + @computed get fontDecoration() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontDecoration) as string; } // prettier-ignore - set _recordingDictation(value) { + set recordingDictation(value) { !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined); } // eslint-disable-next-line no-return-assign - @computed get config() { return FormattedTextBox.MakeConfig(this._rules = new RichTextRules(this.Document, this), this._props); } // prettier-ignore - @computed get _recordingDictation() { return this.dataDoc?.mediaState === mediaState.Recording; } // prettier-ignore - @computed get SidebarShown() { return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } // prettier-ignore + @computed get config() { return FormattedTextBox.MakeConfig(this._rules = new RichTextRules(this.Document, this), this, this._userPlugins ?? []); } // prettier-ignore + @computed get recordingDictation() { return this.dataDoc?.mediaState === mediaState.Recording; } // prettier-ignore + @computed get SidebarShown() { return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } // prettier-ignore @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.sidebarKey]); } // prettier-ignore @computed get noSidebar() { return this.DocumentView?.()._props.hideDecorationTitle || this._props.noSidebar || this.Document._layout_noSidebar; } // prettier-ignore - @computed get layout_sidebarWidthPercent() { return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore - @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4')); } // prettier-ignore + @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, this._showSidebar ? '20%' :'0%'); } // prettier-ignore + @computed get sidebarColor() { return StrCast(this.layoutDoc._sidebar_color, StrCast(this.layoutDoc["_"+this.fieldKey + '_backgroundColor'], '#e4e4e4')); } // prettier-ignore @computed get layout_autoHeight() { return (this._props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this._props.ignoreAutoHeight; } // prettier-ignore - @computed get textHeight() { return NumCast(this.dataDoc[this.fieldKey + '_height']); } // prettier-ignore - @computed get scrollHeight() { return NumCast(this.dataDoc[this.fieldKey + '_scrollHeight']); } // prettier-ignore - @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.dataDoc[this.sidebarKey + '_height']); } // prettier-ignore + @computed get textHeight() { return NumCast(this.layoutDoc["_"+this.fieldKey + '_height']); } // prettier-ignore + @computed get scrollHeight() { return NumCast(this.layoutDoc["_"+this.fieldKey + '_scrollHeight']); } // prettier-ignore + @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.layoutDoc["_"+this.sidebarKey + '_height']); } // prettier-ignore @computed get titleHeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number || 0; } // prettier-ignore @computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } // prettier-ignore @computed get sidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore @@ -215,9 +214,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const rootDoc: Doc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document); - if (!pinProps && this.EditorView?.state.selection.empty) return rootDoc; - const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc }); + if (!pinProps && this.EditorView?.state.selection.empty) return this.rootDoc; + const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc?.title), annotationOn: this.rootDoc }); this.addDocument(anchor); this._finishingLink = true; this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation); @@ -226,18 +224,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return anchor; }; - selectionToFlashcards = async () => { - const queryText = window.getSelection()?.toString() ?? ''; - try { - if (queryText) { - const res = await gptAPICall(queryText, GPTCallType.FLASHCARD); - AnchorMenu.Instance.transferToFlashcard(res || 'Something went wrong', NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)); - } - } catch (err) { - console.error(err); - } - }; - @action setupAnchorMenu = () => { AnchorMenu.Instance.Status = 'marquee'; @@ -257,9 +243,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (target) { anchor.followLinkAudio = true; let stopFunc: () => void = emptyFunction; - const targetData = target[DocData]; - targetData.mediaState = mediaState.Recording; - DictationManager.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore + target.$mediaState = mediaState.Recording; + DictationManager.recordAudioAnnotation(target, Doc.LayoutDataKey(target), stop => { stopFunc = stop }); // prettier-ignore const reactionDisposer = reaction( () => target.mediaState, @@ -321,7 +306,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB leafText = (node: Node) => { if (node.type === this.EditorView?.state.schema.nodes.dashField) { - const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); + const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); const fieldKey = StrCast(node.attrs.fieldKey); return ( (node.attrs.hideKey ? '' : fieldKey + ':') + // @@ -329,7 +314,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); } if (node.type === this.EditorView?.state.schema.nodes.dashDoc) { - const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); + const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); return refDoc[ToString](); } return ''; @@ -441,8 +426,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const oldAutoLinks = Doc.Links(this.Document).filter( link => ((!Doc.isTemplateForField(this.Document) && - (!Doc.isTemplateForField(DocCast(link.link_anchor_1)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && - (!Doc.isTemplateForField(DocCast(link.link_anchor_2)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || + ((DocCast(link.link_anchor_1) && !Doc.isTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && + ((DocCast(link.link_anchor_2) && !Doc.isTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && link.link_relationship === LinkManager.AutoKeywords ); // prettier-ignore @@ -454,7 +439,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB Doc.MyPublishedDocs.filter(term => term.title).forEach(term => { tr = this.hyperlinkTerm(tr, term, newAutoLinks); }); - tr = tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to))); + const marks = tr.storedMarks; + tr = tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to))).setStoredMarks(marks); this.EditorView?.dispatch(tr); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(doc => Doc.DeleteLink?.(doc)); @@ -463,18 +449,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB updateTitle = () => { const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text); if ( - !this._props.dontRegisterView && // (this.Document.isTemplateForField === "text" || !this.Document.isTemplateForField) && // only update the title if the data document's data field is changing + !this._props.dontRegisterView && // only update the title if the data document's data field is changing title.startsWith('-') && this.EditorView && !this.dataDoc.title_custom && - (Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text') + (Doc.LayoutDataKey(this.Document) === this.fieldKey || this.fieldKey === 'text') ) { let node = this.EditorView.state.doc; while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild; const str = node.textContent; const prefix = '-'; - const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title)); + const cfield = ComputedField.DisableCompute(() => FieldValue(this.dataDoc.title)); if (!(cfield instanceof ComputedField)) { this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim(); } @@ -601,7 +587,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } const dragData = de.complete.docDragData; if (dragData) { - const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; + const layoutProto = DocCast(this.layoutDoc.proto); + const dataDoc = layoutProto && Doc.IsDelegateField(layoutProto, this.fieldKey) ? layoutProto : this.dataDoc; const effectiveAcl = GetEffectiveAcl(dataDoc); const draggedDoc = dragData.droppedDocuments.lastElement(); let added: Opt<boolean>; @@ -609,7 +596,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl) && !dragData.draggedDocuments.includes(this.Document)) { // replace text contents when dragging with Alt if (de.altKey) { - const fieldKey = Doc.LayoutFieldKey(draggedDoc); + const fieldKey = Doc.LayoutDataKey(draggedDoc); if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.Document)) { Doc.GetProto(this.dataDoc)[this.fieldKey] = Field.Copy(draggedDoc[fieldKey]); } @@ -709,45 +696,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } updateHighlights = (highlights: string[]) => { - if (Array.from(highlights).join('') === FormattedTextBox._globalHighlightsCache) return; - setTimeout(() => { - FormattedTextBox._globalHighlightsCache = Array.from(highlights).join(''); - }); - clearStyleSheetRules(FormattedTextBox._userStyleSheet); - if (!highlights.includes('Audio Tags')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, ''); - } - if (highlights.includes('Text from Others')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' }); - } - if (highlights.includes('My Text')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' }); - } - if (highlights.includes('Todo Items')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' }); - } - if (highlights.includes('Important Items')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-important', { 'font-size': 'larger' }); - } - if (highlights.includes('Bold Text')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror strong > span', { 'font-size': 'large' }, ''); - addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror :not(strong > span)', { 'font-size': '0px' }, ''); - } - if (highlights.includes('Disagree Items')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-disagree', { 'text-decoration': 'line-through' }); - } - if (highlights.includes('Ignore Items')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-ignore', { 'font-size': '1' }); - } + const userStyleSheet = () => { + if (!this._userStyleSheetElement) { + this._userStyleSheetElement = addStyleSheet(); + } + return this._userStyleSheetElement.sheet; + }; + const viewId = this.DocumentView?.().ViewGuid ?? 1; + const userId = ClientUtils.CurrentUserEmail().replace(/\./g, '').replace('@', ''); // must match marks_rts -> user_mark's uid + highlights.filter(f => f !== 'Audio Tags').length && clearStyleSheetRules(userStyleSheet()); + if (!highlights.includes('Audio Tags')) addStyleSheetRule(userStyleSheet(), `#${viewId} .audiotag`, { display: 'none' }, ''); // prettier-ignore + if (highlights.includes('Text from Others')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-remote`, { background: 'yellow' }, ''); // prettier-ignore + if (highlights.includes('My Text')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { background: 'moccasin' }, ''); // prettier-ignore + if (highlights.includes('Todo Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-todo`, { outline: 'black solid 1px' }, ''); // prettier-ignore + if (highlights.includes('Important Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-important`, { 'font-size': 'larger' }, ''); // prettier-ignore + if (highlights.includes('Disagree Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-disagree`, { 'text-decoration': 'line-through' }, ''); // prettier-ignore + if (highlights.includes('Ignore Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-ignore`, { 'font-size': '1' }, ''); // prettier-ignore + if (highlights.includes('Bold Text')) { addStyleSheetRule(userStyleSheet(), `#${viewId} .formattedTextBox-inner .ProseMirror p:not(:has(strong))`, { 'font-size': '0px' }, ''); + addStyleSheetRule(userStyleSheet(), `#${viewId} .formattedTextBox-inner .ProseMirror p:not(:has(strong)) ::after`, { content: '...', 'font-size': '5px' }, '')} // prettier-ignore if (highlights.includes('By Recent Minute')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' }); + addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { opacity: '0.1' }, ''); const min = Math.round(Date.now() / 1000 / 60); - numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); + numberRange(10).map(i => addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-min-` + (min - i), { opacity: ((10 - i - 1) / 10).toString() }, '')); } if (highlights.includes('By Recent Hour')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' }); + addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { opacity: '0.1' }, ''); const hr = Math.round(Date.now() / 1000 / 60 / 60); - numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); + numberRange(10).map(i => addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-hr-` + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }, '')); } this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView) }; @@ -755,15 +730,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB @action toggleSidebar = (preview: boolean = false) => { const defaultSidebar = 250; - const prevWidth = 1 - this.sidebarWidth() / DivWidth(this._ref.current!); + const dw = DivWidth(this._ref.current); + const prevWidth = 1 - this.sidebarWidth() / dw / this.nativeScaling(); if (preview) this._showSidebar = true; else { - this.layoutDoc[this.sidebarKey + '_freeform_scale_max'] = 1; - this.layoutDoc._layout_showSidebar = - (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(defaultSidebar / (NumCast(this.layoutDoc._width) + defaultSidebar)) * 100}%` : '0%') !== '0%'; + this.layoutDoc._layout_sidebarWidthPercent = + this.sidebarWidthPercent === '0%' // + ? `${(defaultSidebar / (NumCast(this.layoutDoc._width) + defaultSidebar)) * 100}%` // + : '0%'; + this.layoutDoc._layout_showSidebar = this.sidebarWidthPercent !== '0%'; } - this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) + defaultSidebar : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); + this.layoutDoc._width = + !preview && this.SidebarShown // + ? NumCast(this.layoutDoc._width) + defaultSidebar + : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; sidebarDown = (e: React.PointerEvent) => { const batch = UndoManager.StartBatch('toggle sidebar'); @@ -781,7 +762,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { const localDelta = this.DocumentView?.().screenToViewTransform().transformDirection(delta[0], delta[1]) ?? delta; - const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100; + const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.sidebarWidthPercent.replace('%', ''))) / 100; const width = NumCast(this.layoutDoc._width) + localDelta[0]; this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%'; this.layoutDoc.width = width; @@ -859,57 +840,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return; } - const changeItems: ContextMenuProps[] = []; - changeItems.push({ - description: 'plain', - event: undoable(() => { - Doc.setNativeView(this.Document); - this.layoutDoc.layout_autoHeightMargins = undefined; - }, 'set plain view'), - icon: 'eye', - }); - changeItems.push({ - description: 'metadata', - event: undoable(() => { - this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; - this.Document.layout_fieldKey = 'layout_meta'; - setTimeout(() => { - this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50; - }, 50); - }, 'set metadata view'), - icon: 'eye', - }); - const noteTypesDoc = Cast(Doc.UserDoc().template_notes, Doc, null); - DocListCast(noteTypesDoc?.data).forEach(note => { - const icon: IconProp = StrCast(note.icon) as IconProp; - changeItems.push({ - description: StrCast(note.title), - event: undoable( - () => { - this.layoutDoc.layout_autoHeightMargins = undefined; - Doc.setNativeView(this.Document); - DocUtils.makeCustomViewClicked(this.Document, Docs.Create.TreeDocument, StrCast(note.title), note); - }, - `set ${StrCast(note.title)} view}` - ), - icon: icon, - }); - }); const highlighting: ContextMenuProps[] = []; const noviceHighlighting = ['Audio Tags', 'My Text', 'Text from Others', 'Bold Text']; const expertHighlighting = [...noviceHighlighting, 'Important Items', 'Ignore Items', 'Disagree Items', 'By Recent Minute', 'By Recent Hour']; (Doc.noviceMode ? noviceHighlighting : expertHighlighting).forEach(option => highlighting.push({ - description: (!FormattedTextBox._globalHighlights.has(option) ? 'Highlight ' : 'Unhighlight ') + option, + description: (!this._curHighlights.has(option) ? 'Highlight ' : 'Unhighlight ') + option, event: action(() => { e.stopPropagation(); - if (!FormattedTextBox._globalHighlights.has(option)) { - FormattedTextBox._globalHighlights.add(option); + if (!this._curHighlights.has(option)) { + this._curHighlights.add(option); } else { - FormattedTextBox._globalHighlights.delete(option); + this._curHighlights.delete(option); } }), - icon: !FormattedTextBox._globalHighlights.has(option) ? 'highlighter' : 'remove-format', + icon: !this._curHighlights.has(option) ? 'highlighter' : 'remove-format', }) ); const appearance = cm.findByDescription('Appearance...'); @@ -956,20 +901,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB icon: 'expand-arrows-alt', }); - appearanceItems.push({ description: 'Change Style...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' }); - - !Doc.noviceMode && - appearanceItems.push({ - description: 'Make Default Layout', - event: () => { - if (!this.layoutDoc.isTemplateDoc) { - MakeTemplate(this.Document); - } - Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.Document); - Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.Document); - }, - icon: 'eye', - }); !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); const options = cm.findByDescription('Options...'); @@ -992,14 +923,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }, icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', }); - !Doc.noviceMode && - optionItems.push({ - description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`, - event: () => { - this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight; - }, - icon: this.Document._layout_autoHeight ? 'lock' : 'unlock', - }); optionItems.push({ description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers', event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')), @@ -1012,31 +935,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' }); }; - findImageTags = async () => { - const c = this.ProseRef?.getElementsByTagName('img'); - if (c) { - for (const i of c) { - // console.log(canvas.toDataURL()); - // canvas.style.zIndex = '2000000'; - // document.body.appendChild(canvas); - if (i.className !== 'ProseMirror-separator') this.getImageDesc(i.src); - } - } - }; - - getImageDesc = async (u: string) => { - try { - const hrefBase64 = await imageUrlToBase64(u); - const response = await gptImageLabel( - hrefBase64, - 'Make flashcards out of this text and image with each question and answer labeled as question and answer. Do not label each flashcard and do not include asterisks: ' + (this.dataDoc.text as RichTextField)?.Text - ); - AnchorMenu.Instance.transferToFlashcard(response || 'Something went wrong', NumCast(this.dataDoc['x']), NumCast(this.dataDoc['y'])); - } catch (error) { - console.log('Error', error); - } - }; - animateRes = (resIndex: number, newText: string) => { if (resIndex < newText.length) { const marks = this.EditorView?.state.storedMarks ?? []; @@ -1071,14 +969,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; breakupDictation = () => { - if (this.EditorView && this._recordingDictation) { + if (this.EditorView && this.recordingDictation) { this.stopDictation(/* true */); this._break = true; const { state } = this.EditorView; const { to } = state.selection; const updated = TextSelection.create(state.doc, to, to); this.EditorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({}))); - if (this._recordingDictation) { + if (this.recordingDictation) { this.recordDictation(); } } @@ -1104,17 +1002,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; const link = CreateLinkToActiveAudio(textanchorFunc, false).lastElement(); if (link) { - link[DocData].isDictation = true; - const audioanchor = Cast(link.link_anchor_2, Doc, null); - const textanchor = Cast(link.link_anchor_1, Doc, null); + link.$isDictation = true; + const audioanchor = DocCast(link.link_anchor_2); + const textanchor = DocCast(link.link_anchor_1); if (audioanchor) { audioanchor.backgroundColor = 'tan'; const audiotag = this.EditorView.state.schema.nodes.audiotag.create({ timeCode: NumCast(audioanchor._timecodeToShow), audioId: audioanchor[Id], - textId: textanchor[Id], + textId: textanchor?.[Id] ?? '', }); - textanchor[DocData].title = 'dictation:' + audiotag.attrs.timeCode; + textanchor && (textanchor.$title = 'dictation:' + audiotag.attrs.timeCode); const tr = this.EditorView.state.tr.insert(this.EditorView.state.doc.content.size, audiotag); const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); @@ -1249,10 +1147,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc. // Since we also monitor all component height changes, this will update the document's height. - resetNativeHeight = (scrollHeight: number) => { - const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight); - this.dataDoc[this.fieldKey + '_height'] = scrollHeight; - if (nh) this.layoutDoc._nativeHeight = scrollHeight; + resetNativeHeight = action((scrollHeight: number) => { + this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight; + if (!this.layoutDoc.isTemplateForField) this.layoutDoc._nativeHeight = scrollHeight; + }); + + addPlugin = (plugin: Plugin) => { + const editorView = this.EditorView; + if (editorView) { + this._userPlugins.push(plugin); + const newState = editorView.state.reconfigure({ + plugins: [...editorView.state.plugins, plugin], + }); + editorView.updateState(newState); + } }; @computed get tagsHeight() { @@ -1260,7 +1168,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } @computed get contentScaling() { - return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this._props.NativeDimScaling?.() || 1 : 1; + return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this.nativeScaling() : 1; } componentDidMount() { !this._props.dontSelectOnLoad && this._props.setContentViewBox?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. @@ -1270,15 +1178,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB () => ({ 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( - () => Array.from(FormattedTextBox._globalHighlights).slice(), - highlights => this.updateHighlights(highlights), - { fireImmediately: true } - ); - this._disposers.width = reaction( - () => this._props.PanelWidth(), - () => this.tryUpdateScrollHeight() - ); + this._disposers.highlights = reaction(() => Array.from(this._curHighlights).slice(), this.updateHighlights, { fireImmediately: true }); + this._disposers.width = reaction(this._props.PanelWidth, this.tryUpdateScrollHeight); this._disposers.scrollHeight = reaction( () => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }), ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && this.resetNativeHeight(scrollHeight), @@ -1290,7 +1191,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ({ border, sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); if ( - (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && // + (!Array.from(this._curHighlights).includes('Bold Text') || this._props.isSelected()) && // layoutAutoHeight && newHeight && (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height)) && @@ -1299,7 +1200,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._props.setHeight?.(newHeight); } }, - { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') } + { fireImmediately: !Array.from(this._curHighlights).includes('Bold Text') } ); this._disposers.links = reaction( () => Doc.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks @@ -1314,8 +1215,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const dataData = this.dataDoc[this.fieldKey]; const layoutData = Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? undefined : this.layoutDoc[this.fieldKey]; const dataTime = dataData ? (DateCast(this.dataDoc[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; - const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.layoutDoc)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; - const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; + const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.layoutDoc)?.[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; + const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)?.[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData; const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData; return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) }; @@ -1354,7 +1255,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); } this.prepareForTyping(); - if (FormattedTextBox._globalHighlights.has('Bold Text')) { + if (this._curHighlights.has('Bold Text')) { this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed } if (((RichTextMenu.Instance?.view === this.EditorView && this.EditorView) || this.isLabel) && !selected) { @@ -1370,14 +1271,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (!this._props.dontRegisterView) { this._disposers.record = reaction( - () => this._recordingDictation, + () => this.recordingDictation, () => { this.stopDictation(/* true */); - this._recordingDictation && this.recordDictation(); + this.recordingDictation && this.recordDictation(); }, { fireImmediately: true } ); - if (this._recordingDictation) setTimeout(this.recordDictation); + if (this.recordingDictation) setTimeout(this.recordDictation); } this._disposers.scroll = reaction( () => NumCast(this.layoutDoc._layout_scrollTop), @@ -1411,7 +1312,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // } catch (err) { // console.log('Drop failed', err); // } - this.addDocument?.(DocCast(this.Document.image)); + DocCast(this.Document.image) && this.addDocument?.(DocCast(this.Document.image)!); } //if (this.Document.image) this.addDocument?.(DocCast(this.Document.image)); @@ -1434,7 +1335,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } else if (!separated && node.isBlock) { text += '\n'; separated = true; - } else if (node.type.name === 'hard_break') { + } else if (node.type.name === schema.nodes.hard_break.name) { text += '\n'; } }, @@ -1495,42 +1396,57 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } _didScroll = false; _scrollStopper: undefined | (() => void); + scrollToSelection = () => { + if (this.EditorView && this._ref.current) { + const editorView = this.EditorView; + const docPos = editorView.coordsAtPos(editorView.state.selection.to); + const viewRect = this._ref.current.getBoundingClientRect(); + const scrollRef = this._scrollRef; + const topOff = docPos.top < viewRect.top ? docPos.top - viewRect.top : undefined; + const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined; + if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) { + const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE); + const scrollPos = scrollRef.scrollTop + shift * this.ScreenToLocalBoxXf().Scale; + if (this._focusSpeed !== undefined) { + setTimeout(() => { + scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper)); + }); + } else { + scrollRef.scrollTo({ top: scrollPos }); + } + this._didScroll = true; + } + } + return true; + }; // eslint-disable-next-line @typescript-eslint/no-explicit-any setupEditor(config: any, fieldKey: string) { const curText = Cast(this.dataDoc[this.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.fieldKey]); const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { this.EditorView?.destroy(); + const edState = () => { + try { + return rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config); + } catch { + return EditorState.create(config); + } + }; this._editorView = new EditorView(this.ProseRef, { - state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config), - handleScrollToSelection: editorView => { - const docPos = editorView.coordsAtPos(editorView.state.selection.to); - const viewRect = this._ref.current!.getBoundingClientRect(); - const scrollRef = this._scrollRef; - const topOff = docPos.top < viewRect.top ? docPos.top - viewRect.top : undefined; - const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined; - if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) { - const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE); - const scrollPos = scrollRef.scrollTop + shift * this.ScreenToLocalBoxXf().Scale; - if (this._focusSpeed !== undefined) { - setTimeout(() => { - scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper)); - }); - } else { - scrollRef.scrollTo({ top: scrollPos }); - } - this._didScroll = true; - } - return true; - }, + state: edState(), + handleScrollToSelection: this.scrollToSelection, dispatchTransaction: this.dispatchTransaction, nodeViews: FormattedTextBox._nodeViews(this), clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); + // bcz: major hack! a patch to prosemirror broke scrolling to selection when the selection is not a dom selection + // this replaces prosemirror's scrollToSelection function with Dash's + (this.EditorView as unknown as { scrollToSelection: unknown }).scrollToSelection = this.scrollToSelection; const { state } = this._editorView; if (!rtfField) { - const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; + const layoutProto = DocCast(this.layoutDoc.proto); + const dataDoc = layoutProto && Doc.IsDelegateField(layoutProto, this.fieldKey) ? layoutProto : this.dataDoc; const startupText = Field.toString(dataDoc[fieldKey] as FieldType); const textAlign = StrCast(this.dataDoc[this.fieldKey + '_align'], StrCast(Doc.UserDoc().textAlign)) || 'left'; if (textAlign !== 'left') { @@ -1546,25 +1462,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._editorView.TextView = this; } - const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, DocumentView.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())); + const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.rootDoc, DocumentView.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())); const selLoadChar = FormattedTextBox.SelectOnLoadChar; if (selectOnLoad) { DocumentView.SetSelectOnLoad(undefined); FormattedTextBox.SelectOnLoadChar = ''; } if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { - const $from = this.EditorView.state.selection.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined; + const { $from } = this.EditorView.state.selection; const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? []; const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; - let { tr } = this.EditorView.state; - if (selLoadChar) { - const tr1 = this.EditorView.state.tr.setStoredMarks(storedMarks); - tr = selLoadChar === 'Enter' ? tr1.insert(this.EditorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this.EditorView.state.doc.content.size - 1); - } - this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size - 1))).setStoredMarks(storedMarks)); + if (selLoadChar === 'Enter') { + splitBlock(this.EditorView.state, (tx3: Transaction) => this.EditorView?.dispatch(tx3.setStoredMarks(storedMarks))); + } else if (selLoadChar) { + this.EditorView.dispatch(this.EditorView.state.tr.replaceSelectionWith(this.EditorView.state.schema.text(selLoadChar, storedMarks))); // prettier-ignore + } else this.EditorView.dispatch(this.EditorView.state.tr.setStoredMarks(storedMarks)); this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data - console.log(this.EditorView.state); } if (selectOnLoad) { this.EditorView!.focus(); @@ -1591,9 +1505,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; componentWillUnmount() { - if (this._recordingDictation) { - this._recordingDictation = !this._recordingDictation; + if (this.recordingDictation) { + this.recordingDictation = !this.recordingDictation; } + removeStyleSheet(this._userStyleSheetElement); Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); FormattedTextBox.LiveTextUndo?.end(); @@ -1627,7 +1542,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }); } - if (this._recordingDictation && !e.ctrlKey && e.button === 0) { + if (this.recordingDictation && !e.ctrlKey && e.button === 0) { this.breakupDictation(); } FormattedTextBoxComment.textBox = this; @@ -1785,8 +1700,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB */ tryKeepingFocus = (target: Element | null) => { for (let newFocusEle = target instanceof HTMLElement ? target : null; newFocusEle; newFocusEle = newFocusEle?.parentElement) { - // test if parent of new focused element is a UI button (should be more specific than testing className) - if (newFocusEle?.className === 'fonticonbox' || newFocusEle?.className === 'popup-container') { + // bcz: HACK!! test if parent of new focused element is a UI button (should be more specific than testing className) + if (['fonticonbox', 'antimodeMenu-cont', 'popup-container'].includes(newFocusEle?.className ?? '')) { return this.EditorView?.focus(); // keep focus on text box } } @@ -1840,13 +1755,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } const { state } = _editorView; if (!state.selection.empty && e.key === '%') { - this._rules!.EnteringStyle = true; + this._enteringStyle = true; StopEvent(e); return; } + if (this._enteringStyle && 'tix!'.includes(e.key)) { + const tag = e.key === 't' ? 'todo' : e.key === 'i' ? 'ignore' : e.key === 'x' ? 'disagree' : e.key === '!' ? 'important' : '??'; + const node = state.selection.$from.nodeAfter; + const start = state.selection.from; + const end = state.selection.to; + + if (node) { + StopEvent(e); + _editorView.dispatch( + state.tr + .removeMark(start, end, schema.marks.user_mark) + .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })) + .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag, modified: Math.round(Date.now() / 1000 / 60) })) + ); + return; + } + } - if (state.selection.empty || !this._rules!.EnteringStyle) { - this._rules!.EnteringStyle = false; + if (state.selection.empty || !this._enteringStyle) { + this._enteringStyle = false; } for (let i = state.selection.from; i <= state.selection.to; i++) { const node = state.doc.resolve(i); @@ -1910,10 +1842,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation const setScrollHeight = () => { - this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight; + this.layoutDoc['_' + this.fieldKey + '_scrollHeight'] = scrollHeight; }; - if (this.Document === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { + if (this.Document === this.layoutDoc || this.layoutDoc.rootDocument) { setScrollHeight(); } else { setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... @@ -1922,7 +1854,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }; fitContentsToBox = () => BoolCast(this.Document._freeform_fitContentsToBox); - sidebarContentScaling = () => (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + nativeScaling = () => this._props.NativeDimScaling?.() || 1; sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.sidebarKey) => { if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); @@ -1930,17 +1862,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.sidebarKey); sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.sidebarKey); setSidebarHeight = (height: number) => { - this.dataDoc[this.sidebarKey + '_height'] = height; + this.layoutDoc['_' + this.sidebarKey + '_height'] = height; }; - sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); sidebarScreenToLocal = () => this._props .ScreenToLocalTransform() - .translate(-(this._props.PanelWidth() - this.sidebarWidth()) / (this._props.NativeDimScaling?.() || 1), 0) - .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this._props.NativeDimScaling?.() || 1)); + .translate(-(this._props.PanelWidth() - this.sidebarWidth()) / this.nativeScaling(), 0) + .scale(1 / this.nativeScaling()); @computed get audioHandle() { - return !this._recordingDictation ? null : ( + return !this.recordingDictation ? null : ( <div className="formattedTextBox-dictation" onPointerDown={e => @@ -1950,7 +1882,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB returnFalse, emptyFunction, action(() => { - this._recordingDictation = !this._recordingDictation; + this.recordingDictation = !this.recordingDictation; }) ) }> @@ -1958,6 +1890,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB </div> ); } + private _sideBtnWidth = 35; + /** + * How much the content of the view is being scaled based on its nesting and its fit-to-width settings + */ + @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale ; } // 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 maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.2 * this._props.PanelWidth())*this.viewScaling; } // prettier-ignore + /** + * 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 uiBtnScaling() { return this.maxWidgetSize / this._sideBtnWidth; } // prettier-ignore + @computed get sidebarHandle() { TraceMobx(); const annotated = DocListCast(this.dataDoc[this.sidebarKey]).filter(d => d?.author).length; @@ -1972,6 +1918,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB backgroundColor, color, opacity: annotated ? 1 : undefined, + transform: `scale(${this.uiBtnScaling})`, }}> <FontAwesomeIcon icon="comment-alt" /> </div> @@ -1985,7 +1932,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB <SidebarAnnos ref={this._sidebarRef} {...this._props} - Document={this.Document} + Doc={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} usePanelWidth @@ -2016,7 +1963,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB isAnnotationOverlay={false} select={emptyFunction} isAnyChildContentActive={returnFalse} - NativeDimScaling={this.sidebarContentScaling} + NativeDimScaling={this.nativeScaling} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.sidebarRemDocument} moveDocument={this.sidebarMoveDocument} @@ -2033,7 +1980,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); }; return ( - <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> {renderComponent(StrCast(this.layoutDoc[this.sidebarKey + '_type_collection']))} </div> ); @@ -2081,7 +2028,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return this._fieldKey; } @computed get _fieldKey() { - const usePath = this._props.ignoreUsePath ? '' : StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]); + const usePath = StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]); return this._props.fieldKey + (usePath && (!usePath.includes(':hover') || this._props.isHovering?.() || this._props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : ''); } onPassiveWheel = (e: WheelEvent) => { @@ -2091,7 +2038,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) if (this._props.isContentActive()) { - const scale = this._props.NativeDimScaling?.() || 1; + const scale = this.nativeScaling(); 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' > const height = Number(styleFromLayout.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable @@ -2108,15 +2055,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB render() { TraceMobx(); - const scale = this._props.NativeDimScaling?.() || 1; - const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; + const scale = this.nativeScaling(); + const rounded = StrCast(this.layoutDoc._layout_borderRounding) === '100%' ? '-rounded' : ''; setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); - 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 paddingX = Math.max(NumCast(this.layoutDoc._xMargin), 0, this._props.xPadding ?? 0, this._props.screenXPadding?.(this._props.DocumentView?.()) ?? 0); + const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, this._props.yPadding ?? 0); // prettier-ignore 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 this.isLabel ? ( <LabelBox {...this._props} /> @@ -2137,7 +2081,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB height: `${100 / scale}%`, }), transition: 'inherit', - paddingBottom: this.tagsHeight, // overflowY: this.layoutDoc._layout_autoHeight ? "hidden" : undefined, color: this.fontColor, fontSize: this.fontSize, @@ -2170,13 +2113,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._scrollRef = r; }} style={{ - width: this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, + width: this.noSidebar ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR || this.layoutDoc._layout_hideScroll ? 'hidden' : this.layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} onDrop={this.ondrop}> <div - className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered && this.scrollHeight <= (this._props.fitWidth?.(this.Document) ? this._props.PanelHeight() : NumCast(this.layoutDoc._height)) ? 'centered' : ''} ${this.layoutDoc.hCentering}`} + className={`formattedTextBox-inner${rounded} ${this.dataDoc.text_centered && this.scrollHeight <= (this._props.fitWidth?.(this.Document) ? this._props.PanelHeight() : NumCast(this.layoutDoc._height)) ? 'centered' : ''} ${this.layoutDoc.hCentering}`} ref={this.createDropTarget} style={{ padding: StrCast(this.layoutDoc._textBoxPadding), @@ -2190,7 +2133,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }} /> </div> - {this.noSidebar || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} + {this.noSidebar || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection} {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden || this.Document.quiz ? null : this.sidebarHandle} {this.audioHandle} {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null} diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 6c0eac103..1d790f5bb 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -134,20 +134,16 @@ export class FormattedTextBoxComment { // this checks if the selection is a hyperlink. If so, it displays the target doc's text for internal links, and the url of the target for external links. if (state.selection.$from && hrefs?.length) { - const nbef = findStartOfMark(state.selection.$from, view, findLinkMark); - const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef; - // nbef && - naft && - LinkInfo.SetLinkInfo({ - DocumentView: textBox.DocumentView, - styleProvider: textBox._props.styleProvider, - linkSrc: textBox.Document, - linkDoc: linkDoc ? (DocServer.GetCachedRefField(linkDoc) as Doc) : undefined, - location: (pos => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))), - hrefs, - showHeader: true, - noPreview, - }); + LinkInfo.SetLinkInfo({ + DocumentView: textBox.DocumentView, + styleProvider: textBox._props.styleProvider, + linkSrc: textBox.Document, + linkDoc: linkDoc ? (DocServer.GetCachedRefField(linkDoc) as Doc) : undefined, + location: (pos => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, 0 - 1))), + hrefs, + showHeader: true, + noPreview, + }); } } diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 3c84e5a10..7ae1fc202 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -1,29 +1,30 @@ import { chainCommands, deleteSelection, exitCode, joinBackward, joinDown, joinUp, lift, newlineInCode, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from 'prosemirror-commands'; import { redo, undo } from 'prosemirror-history'; -import { Schema } from 'prosemirror-model'; +import { MarkType, Node, Schema } from 'prosemirror-model'; import { liftListItem, sinkListItem, splitListItem, wrapInList } from 'prosemirror-schema-list'; -import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state'; +import { Command, EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state'; import { liftTarget } from 'prosemirror-transform'; import { EditorView } from 'prosemirror-view'; import { ClientUtils } from '../../../../ClientUtils'; -import { Utils } from '../../../../Utils'; +import { numberRange, Utils } from '../../../../Utils'; import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols'; import { GetEffectiveAcl } from '../../../../fields/util'; import { Docs } from '../../../documents/Documents'; import { RTFMarkup } from '../../../util/RTFMarkup'; import { DocumentView } from '../DocumentView'; import { OpenWhere } from '../OpenWhere'; +import { FormattedTextBox } from './FormattedTextBox'; const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false; -export type KeyMap = { [key: string]: any }; +export type KeyMap = { [key: string]: Command }; -export const updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?: string, from?: number, to?: number) => { +export function updateBullets(tx2: Transaction, schema: Schema, assignedMapStyle?: string, from?: number, to?: number) { let mapStyle = assignedMapStyle; - tx2.doc.descendants((node: any, offset: any /* , index: any */) => { + tx2.doc.descendants((node: Node, offset: number /* , index: any */) => { if ((from === undefined || to === undefined || (from <= offset + node.nodeSize && to >= offset)) && (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item)) { - const { path } = tx2.doc.resolve(offset) as any; - let depth = Array.from(path).reduce((p: number, c: any) => p + (c.type === schema.nodes.ordered_list ? 1 : 0), 0); + const resolved = tx2.doc.resolve(offset); + let depth = [0, ...numberRange(resolved.depth)].reduce((p, c, idx) => p + (resolved.node(idx).type === schema.nodes.ordered_list ? 1 : 0), 0); if (node.type === schema.nodes.ordered_list) { if (depth === 0 && !assignedMapStyle) mapStyle = node.attrs.mapStyle; depth++; @@ -32,28 +33,29 @@ export const updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle } }); return tx2; -}; +} -export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMap { - const keys: { [key: string]: any } = {}; +export function buildKeymap<S extends Schema<string>>(schema: S, tbox?: FormattedTextBox): KeyMap { + const keys: { [key: string]: Command } = {}; - function bind(key: string, cmd: any) { + function bind(key: string, cmd: Command) { keys[key] = cmd; } function onKey(): boolean | undefined { - // bcz: this is pretty hacky -- prosemirror doesn't send us the keyboard event, but the 'event' variable is in scope.. so we access it anyway + // bcz: hack -- prosemirror doesn't send us the keyboard event, but the 'event' variable is in scope.. so we access it anyway // eslint-disable-next-line no-restricted-globals - return props.onKey?.(event, props); + return event && tbox?._props.onKey?.(event as unknown as KeyboardEvent, tbox); } - const canEdit = (state: any) => { - const permissions = GetEffectiveAcl(props.TemplateDataDocument ?? props.Document[DocData]); - switch (permissions) { + const canEdit = (state: EditorState) => { + if (!tbox) return true; + switch (GetEffectiveAcl(tbox.dataDoc)) { case AclAugment: { - const prevNode = state.selection.$cursor.nodeBefore; - const prevUser = !prevNode ? ClientUtils.CurrentUserEmail() : prevNode.marks.lastElement()?.attrs.userid; + // previously used hack: (state.selection as any).$cursor.nodeBefore; + const prevNode = state.selection?.$anchor.nodeBefore; + const prevUser = !prevNode ? ClientUtils.CurrentUserEmail() : Array.from(prevNode.marks).lastElement()?.attrs.userid; if (prevUser !== ClientUtils.CurrentUserEmail()) { return false; } @@ -64,7 +66,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa return true; }; - const toggleEditableMark = (mark: any) => (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && toggleMark(mark)(state, dispatch); + const toggleEditableMark = (mark: MarkType) => (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => canEdit(state) && toggleMark(mark)(state, dispatch); // History commands bind('Mod-z', undo); @@ -84,13 +86,13 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa bind('Mod-U', toggleEditableMark(schema.marks.underline)); // Commands for lists - bind('Ctrl-i', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state as any, dispatch as any)); + bind('Ctrl-i', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state, dispatch)); bind('Ctrl-Tab', () => onKey() || true); bind('Alt-Tab', () => onKey() || true); bind('Meta-Tab', () => onKey() || true); bind('Meta-Enter', () => onKey() || true); - bind('Tab', (state: EditorState, dispatch: (tx: Transaction) => void) => { + bind('Tab', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { if (onKey()) return true; if (!canEdit(state)) return true; const ref = state.selection; @@ -101,13 +103,13 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa const tx3 = updateBullets(tx2, schema); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); - dispatch(tx3); + dispatch?.(tx3); }) ) { // couldn't sink into an existing list, so wrap in a new one const newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end))); if ( - !wrapInList(schema.nodes.ordered_list)(newstate.state as any, (tx2: Transaction) => { + !wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => { const tx25 = updateBullets(tx2, schema); const olNode = tx25.doc.nodeAt(range!.start)!; const tx3 = tx25.setNodeMarkup(range!.start, olNode.type, olNode.attrs, marks); @@ -115,16 +117,16 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); const tx4 = tx3.setSelection(TextSelection.near(tx3.doc.resolve(state.selection.to + 2))); - dispatch(tx4); + dispatch?.(tx4); }) ) { console.log('bullet promote fail'); } } - return undefined; + return false; }); - bind('Shift-Tab', (state: EditorState, dispatch: (tx: Transaction) => void) => { + bind('Shift-Tab', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { if (onKey()) return true; if (!canEdit(state)) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); @@ -134,119 +136,136 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa const tx3 = updateBullets(tx2, schema); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); - dispatch(tx3); + dispatch?.(tx3); }) ) { console.log('bullet demote fail'); } - return undefined; + return false; }); // Command to create a new Tab with a PDF of all the command shortcuts - bind('Mod-/', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + bind('Mod-/', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { const newDoc = Docs.Create.PdfDocument(ClientUtils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 }); - props.addDocTab(newDoc, OpenWhere.addRight); + tbox?._props.addDocTab(newDoc, OpenWhere.addRight); + return false; }); // Commands to modify BlockType - bind('Ctrl->', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state && wrapIn(schema.nodes.blockquote)(state as any, dispatch as any))); - bind('Alt-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state as any, dispatch as any)); - bind('Shift-Ctrl-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.code_block)(state as any, dispatch as any)); + bind('Ctrl->', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => canEdit(state) && wrapIn(schema.nodes.blockquote)(state, dispatch)); + bind('Alt-\\', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state, dispatch)); + bind('Shift-Ctrl-\\', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => canEdit(state) && setBlockType(schema.nodes.code_block)(state, dispatch)); - bind('Ctrl-m', (state: EditorState, dispatch: (tx: Transaction) => void) => { + bind('Ctrl-m', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { if (canEdit(state)) { const tr = state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: 'math' + Utils.GenerateGuid() })); - dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1)))); + dispatch?.(tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1)))); + return true; } + return false; }); for (let i = 1; i <= 6; i++) { - bind('Shift-Ctrl-' + i, (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state as any, dispatch as any)); + bind('Shift-Ctrl-' + i, (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state, dispatch)); } // Command to create a horizontal break line const hr = schema.nodes.horizontal_rule; - bind('Mod-_', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView())); + bind('Mod-_', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { + if (canEdit(state)) { + dispatch?.(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); + return true; + } + return false; + }); // Command to unselect all - bind('Escape', (state: EditorState, dispatch: (tx: Transaction) => void) => { - dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); - (document.activeElement as any).blur?.(); + bind('Escape', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { + dispatch?.(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); + (document.activeElement as HTMLElement)?.blur?.(); DocumentView.DeselectAll(); + return true; }); bind('Alt-Enter', () => onKey() || true); bind('Ctrl-Enter', () => onKey() || true); - bind('Cmd-a', (state: EditorState, dispatch: (tx: Transaction) => void) => { - dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1)))); + bind('Cmd-a', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { + dispatch?.(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1)))); return true; }); bind('Cmd-?', () => { RTFMarkup.Instance.setOpen(true); return true; }); - bind('Cmd-e', (state: EditorState, dispatch: (tx: Transaction) => void) => { + bind('Cmd-e', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { if (!state.selection.empty) { const mark = state.schema.marks.summarizeInclusive.create(); const tr = state.tr.addMark(state.selection.$from.pos, state.selection.$to.pos, mark); const content = tr.selection.content(); - tr.selection.replaceWith(tr, schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() })); - dispatch(tr); + tr.selection.replaceWith(tr, schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }, undefined, state.selection.$anchor.marks() ?? [])); + dispatch?.(tr); } return true; }); - bind('Cmd-]', (state: EditorState, dispatch: (tx: Transaction) => void) => { - const resolved = state.doc.resolve(state.selection.from) as any; - const { tr } = state; - if (resolved?.parent.type.name === 'paragraph') { - tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'right' }, resolved.parent.marks); + bind('Cmd-]', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { + const { + tr, + selection: { $from }, + } = state; + if ($from?.parent.type.name === 'paragraph') { + tr.setNodeMarkup(state.selection.from - state.selection.$from.parentOffset - 1, schema.nodes.paragraph, { ...$from.parent.attrs, align: 'right' }, $from.parent.marks); } else { - const node = resolved.nodeAfter; + const node = $from.nodeAfter; const sm = state.storedMarks || undefined; if (node) { tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm || [])]); } } - dispatch(tr); + dispatch?.(tr); return true; }); - bind('Cmd-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => { - const resolved = state.doc.resolve(state.selection.from) as any; - const { tr } = state; - if (resolved?.parent.type.name === 'paragraph') { - tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'center' }, resolved.parent.marks); + bind('Cmd-\\', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { + const { + tr, + selection: { $from }, + } = state; + if ($from?.parent.type.name === 'paragraph') { + tr.setNodeMarkup(state.selection.from - state.selection.$from.parentOffset - 1, schema.nodes.paragraph, { ...$from.parent.attrs, align: 'center' }, $from.parent.marks); } else { - const node = resolved.nodeAfter; + const node = $from.nodeAfter; const sm = state.storedMarks || undefined; if (node) { tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm || [])]); } } - dispatch(tr); + dispatch?.(tr); return true; }); - bind('Cmd-[', (state: EditorState, dispatch: (tx: Transaction) => void) => { - const resolved = state.doc.resolve(state.selection.from) as any; - const { tr } = state; - if (resolved?.parent.type.name === 'paragraph') { - tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'left' }, resolved.parent.marks); + bind('Cmd-[', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { + const { + tr, + selection: { $from }, + } = state; + if ($from?.parent.type.name === 'paragraph') { + tr.setNodeMarkup(state.selection.from - state.selection.$from.parentOffset - 1, schema.nodes.paragraph, { ...$from.parent.attrs, align: 'left' }, $from.parent.marks); } else { - const node = resolved.nodeAfter; + const node = $from.nodeAfter; const sm = state.storedMarks || undefined; if (node) { tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm || [])]); } } - dispatch(tr); + dispatch?.(tr); return true; }); - bind('Cmd-f', (state: EditorState, dispatch: (tx: Transaction) => void) => { + bind('Cmd-f', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { const content = state.tr.selection.empty ? undefined : state.tr.selection.content().content.textBetween(0, state.tr.selection.content().size + 1); const newNode = schema.nodes.footnote.create({}, content ? state.schema.text(content) : undefined); const { tr } = state; tr.replaceSelectionWith(newNode); // replace insertion with a footnote. - dispatch( + dispatch?.( tr.setSelection( new NodeSelection( // select the footnote node to open its display tr.doc.resolve( @@ -259,25 +278,25 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa return true; }); - bind('Ctrl-a', (state: EditorState, dispatch: (tx: Transaction) => void) => { - dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1)))); + bind('Ctrl-a', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => { + dispatch?.(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1)))); return true; }); // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); - const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView) => { + const backspace = (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined, view?: EditorView) => { if (onKey()) return true; if (!canEdit(state)) return true; if ( !deleteSelection(state, (tx: Transaction) => { - dispatch(updateBullets(tx, schema)); + dispatch?.(updateBullets(tx, schema)); }) ) { if ( !joinBackward(state, (tx: Transaction) => { - dispatch(updateBullets(tx, schema)); - if (view.state.selection.$anchor.node(-1)?.type === schema.nodes.list_item) { + dispatch?.(updateBullets(tx, schema)); + if (view?.state.selection.$anchor.node(-1)?.type === schema.nodes.list_item) { // gets rid of an extra paragraph when joining two list items together. joinBackward(view.state, (tx2: Transaction) => view.dispatch(tx2)); } @@ -285,7 +304,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa ) { if ( !selectNodeBackward(state, (tx: Transaction) => { - dispatch(updateBullets(tx, schema)); + dispatch?.(updateBullets(tx, schema)); }) ) { return false; @@ -299,7 +318,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa // newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock // command to break line - const enter = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView, once = true) => { + const enter = (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined, view?: EditorView, once = true) => { if (onKey()) return true; if (!canEdit(state)) return true; @@ -311,31 +330,31 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa !state.selection.$from.node().content.size && trange ) { - dispatch(state.tr.lift(trange, depth) as any); + dispatch?.(state.tr.lift(trange, depth)); return true; } const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); - if (!newlineInCode(state, dispatch as any)) { - const olNode = view.state.selection.$anchor.node(-2); - const liNode = view.state.selection.$anchor.node(-1); + if (!newlineInCode(state, dispatch)) { + const olNode = view?.state.selection.$anchor.node(-2); + const liNode = view?.state.selection.$anchor.node(-1); // prettier-ignore if (liNode?.type === schema.nodes.list_item && !liNode.textContent && - olNode?.type === schema.nodes.ordered_list && once && view.state.selection.$from.depth === 3) + olNode?.type === schema.nodes.ordered_list && once && view?.state.selection.$from.depth === 3) { // handles case of hitting enter at then end of a top-level empty list item - the result is to create a paragraph - for (let i = 0; i < 10 && view.state.selection.$from.depth > 1 && liftListItem(schema.nodes.list_item)(view.state, view.dispatch); i++); + for (let i = 0; i < 10 && view?.state.selection.$from.depth > 1 && liftListItem(schema.nodes.list_item)(view.state, view.dispatch); i++); } else if ( - !splitListItem(schema.nodes.list_item)(state as any, (tx2: Transaction) => { + !splitListItem(schema.nodes.list_item)(state, (tx2: Transaction) => { const tx3 = updateBullets(tx2, schema); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); - dispatch(tx3); + dispatch?.(tx3); // removes an extra paragraph created when selecting text across two list items or splitting an empty list item - !once && view.dispatch(view.state.tr.deleteRange(view.state.selection.from - 5, view.state.selection.from - 2)); + !once && view?.dispatch(view?.state.tr.deleteRange(view.state.selection.from - 5, view.state.selection.from - 2)); }) ) { - if (once && view.state.selection.$from.node(-2)?.type === schema.nodes.ordered_list && view.state.selection.$from.node(-1)?.type === schema.nodes.list_item && view.state.selection.$from.node(-1)?.textContent === '') { + if (once && view?.state.selection.$from.node(-2)?.type === schema.nodes.ordered_list && view?.state.selection.$from.node(-1)?.type === schema.nodes.list_item && view.state.selection.$from.node(-1)?.textContent === '') { // handles case of hitting enter on an empty list item which needs to create a second empty paragraph, then split it by calling enter() again view.dispatch(view.state.tr.insert(view.state.selection.from, schema.nodes.paragraph.create({}))); enter(view.state, view.dispatch, view, false); @@ -346,12 +365,12 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa const tonode = tx3.selection.$to.node(); if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) { const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks).setStoredMarks(marks || []); - dispatch(tx4); + dispatch?.(tx4); } - if (view.state.selection.$anchor.depth > 0 && - view.state.selection.$anchor.node(view.state.selection.$anchor.depth-1).type === schema.nodes.list_item && - view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) { + if ((view?.state.selection.$anchor.depth ??0) > 0 && + view?.state.selection.$anchor.node(view.state.selection.$anchor.depth-1).type === schema.nodes.list_item && + view?.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) { // if text is selected across list items, then we need to forcibly insert a new line since the splitBlock code joins the two list items. enter(view.state, dispatch, view, false); } @@ -368,14 +387,14 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa // Command to create a blank space bind('Space', () => { - const editDoc = props.TemplateDataDocument ?? props.Document[DocData]; + const editDoc = tbox?._props.TemplateDataDocument ?? tbox?.Document[DocData]; if (editDoc && ![AclAdmin, AclAugment, AclEdit].includes(GetEffectiveAcl(editDoc))) return true; return false; }); - bind('Alt-ArrowUp', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && joinUp(state, dispatch as any)); - bind('Alt-ArrowDown', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && joinDown(state, dispatch as any)); - bind('Mod-BracketLeft', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && lift(state, dispatch as any)); + bind('Alt-ArrowUp', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => canEdit(state) && joinUp(state, dispatch)); + bind('Alt-ArrowDown', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => canEdit(state) && joinDown(state, dispatch)); + bind('Mod-BracketLeft', (state: EditorState, dispatch: ((tx: Transaction) => void) | undefined) => canEdit(state) && lift(state, dispatch)); const cmd = chainCommands(exitCode, (state, dispatch) => { if (dispatch) { diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 758b4035e..c7731e812 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -117,7 +117,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return this._activeAlignment; } @computed get textVcenter() { - return BoolCast(this.dataDoc?._layout_centered, BoolCast(Doc.UserDoc().layout_centered)); + return BoolCast(this.dataDoc?.text_centered, BoolCast(Doc.UserDoc().textCentered)); } @action @@ -148,11 +148,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this._activeAlignment = this.getActiveAlignment(); this._activeFitBox = BoolCast(refDoc[refField + 'fitBox'], BoolCast(Doc.UserDoc().fitBox)); this._activeFontFamily = !activeFamilies.length - ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(this.dataDoc?.[Doc.LayoutFieldKey(this.dataDoc) + '_fontFamily'], refVal('fontFamily', 'Arial'))) + ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(this.dataDoc?.[Doc.LayoutDataKey(this.dataDoc) + '_fontFamily'], refVal('fontFamily', 'Arial'))) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; - this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(this.dataDoc?.[Doc.LayoutFieldKey(this.dataDoc) + '_fontSize'], refVal('fontSize', '10px'))) : activeSizes[0]; + this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(this.dataDoc?.[Doc.LayoutDataKey(this.dataDoc) + '_fontSize'], refVal('fontSize', '10px'))) : activeSizes[0]; this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, refVal('fontColor', 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...'; @@ -367,7 +367,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { if (this.TextView && this.view && fontField !== 'fitBox') { - if (this.view.hasFocus()) { + const anchorNode = window.getSelection()?.anchorNode; + if (this.view.hasFocus() || (anchorNode && this.TextView.ProseRef?.contains(anchorNode))) { const attrs: { [key: string]: string } = {}; attrs[fontField] = value; const fmark = this.view.state.schema.marks['pF' + fontField.substring(1)].create(attrs); @@ -381,7 +382,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }); } } else if (this.dataDoc) { - this.dataDoc[`${Doc.LayoutFieldKey(this.dataDoc)}_${fontField}`] = value; + this.dataDoc[`${Doc.LayoutDataKey(this.dataDoc)}_${fontField}`] = value; this.updateMenu(undefined, undefined, undefined, this.dataDoc); } else { Doc.UserDoc()[fontField] = value; @@ -413,20 +414,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.view.focus(); }; - insertSummarizer(state: EditorState, dispatch: (tr: Transaction) => void) { - if (state.selection.empty) return false; - const mark = state.schema.marks.summarize.create(); - const { tr } = state; - tr.addMark(state.selection.from, state.selection.to, mark); - const content = tr.selection.content(); - const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }); - dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); - return true; - } - vcenterToggle = () => { - if (this.dataDoc) this.dataDoc._layout_centered = !this.dataDoc._layout_centered; - else Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered; + if (this.dataDoc) this.dataDoc.text_centered = !this.dataDoc.text_centered; + else Doc.UserDoc().textCentered = !Doc.UserDoc().textCentered; }; align = (view: EditorView | undefined, dispatch: undefined | ((tr: Transaction) => void), alignment: 'left' | 'right' | 'center') => { if (view && dispatch && this.RootSelected) { diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index c332c592b..26ccf6931 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -1,7 +1,6 @@ import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules'; import { NodeType } from 'prosemirror-model'; import { NodeSelection, TextSelection } from 'prosemirror-state'; -import { ClientUtils } from '../../../../ClientUtils'; import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; @@ -21,7 +20,6 @@ import { schema } from './schema_rts'; export class RichTextRules { public Document: Doc; public TextBox: FormattedTextBox; - public EnteringStyle: boolean = false; constructor(doc: Doc, textBox: FormattedTextBox) { this.Document = doc; this.TextBox = textBox; @@ -84,9 +82,8 @@ export class RichTextRules { // Create annotation to a field on the text document new InputRule(/>::$/, (state, match, start, end) => { const creator = (doc: Doc) => { - const textDoc = this.Document[DocData]; - const numInlines = NumCast(textDoc.inlineTextCount); - textDoc.inlineTextCount = numInlines + 1; + const numInlines = NumCast(this.Document.$inlineTextCount); + this.Document.$inlineTextCount = numInlines + 1; const node = state.doc.resolve(start).nodeAfter; const newNode = schema.nodes.dashComment.create({ docId: doc[Id], reflow: false }); const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: doc[Id], float: 'right' }); @@ -109,28 +106,25 @@ export class RichTextRules { }), // Create annotation to a field on the text document new InputRule(/>>$/, (state, match, start, end) => { - const textDoc = this.Document[DocData]; - const numInlines = NumCast(textDoc.inlineTextCount); - textDoc.inlineTextCount = numInlines + 1; + const numInlines = NumCast(this.Document.$inlineTextCount); + this.Document.$inlineTextCount = numInlines + 1; const inlineFieldKey = 'inline' + numInlines; // which field on the text document this annotation will write to const inlineLayoutKey = 'layout_' + inlineFieldKey; // the field holding the layout string that will render the inline annotation const textDocInline = Docs.Create.TextDocument('', { - _layout_fieldKey: inlineLayoutKey, _width: 75, _height: 35, - annotationOn: textDoc, _layout_fitWidth: true, _layout_autoHeight: true, - text_fontSize: '9px', - title: 'inline comment', }); + textDocInline.layout_fieldKey = inlineLayoutKey; + textDocInline.annotationOn = this.Document[DocData]; textDocInline.title = inlineFieldKey; // give the annotation its own title textDocInline.title_custom = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc - textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point + //textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point textDocInline.isDataDoc = true; - textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] - textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text - textDoc[inlineFieldKey] = ''; // set a default value for the annotation + textDocInline.proto = this.Document[DocData]; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] + this.Document['$' + inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text + this.Document['$' + inlineFieldKey] = ''; // set a default value for the annotation const node = state.doc.resolve(start).nodeAfter; const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id], reflow: true }); const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' }); @@ -145,9 +139,8 @@ export class RichTextRules { return replaced; }), - // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode) - new InputRule(/(%d|d)$/, (state, match, start, end) => { - if (!match[0].startsWith('%') && !this.EnteringStyle) return null; + // set the First-line indent node type for the selection's paragraph + new InputRule(/%d$/, (state, match, start, end) => { const pos = state.doc.resolve(start); for (let depth = pos.depth; depth >= 0; depth--) { const node = pos.node(depth); @@ -160,9 +153,8 @@ export class RichTextRules { return null; }), - // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) - new InputRule(/(%h|h)$/, (state, match, start, end) => { - if (!match[0].startsWith('%') && !this.EnteringStyle) return null; + // set the Hanging indent node type for the current selection's paragraph + new InputRule(/%h$/, (state, match, start, end) => { const pos = state.doc.resolve(start); for (let depth = pos.depth; depth >= 0; depth--) { const node = pos.node(depth); @@ -175,9 +167,8 @@ export class RichTextRules { return null; }), - // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) - new InputRule(/(%q|q)$/, (state, match, start, end) => { - if (!match[0].startsWith('%') && !this.EnteringStyle) return null; + // set the Quoted indent node type for the current selection's paragraph + new InputRule(/%q$/, (state, match, start, end) => { const pos = state.doc.resolve(start); if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) { const { node } = state.selection; @@ -294,7 +285,8 @@ export class RichTextRules { editor.dispatch(estate.tr.setSelection(new TextSelection(estate.doc.resolve(start), estate.doc.resolve(end - prefixLength)))); } - DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' }); + const tanchor = this.TextBox.getAnchor(true); + tanchor && DocUtils.MakeLink(tanchor, target, { link_relationship: 'portal to:portal from' }); const teditor = this.TextBox.EditorView; if (teditor && selection) { @@ -319,10 +311,10 @@ export class RichTextRules { }), // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document - // [@{this,doctitle,}.fieldKey{:,=,:=,=:=}value] - // [@{this,doctitle,}.fieldKey] + // @{this,doctitle,}.fieldKey{:,=,:=,=:=}value + // @{this,doctitle,}.fieldKey new InputRule( - /\[(@|@this\.|@[a-zA-Z_? \-0-9]+\.)([a-zA-Z_?\-0-9]+)((:|=|:=|=:=)([a-zA-Z,_().@?+\-*/ 0-9()]*))?\]/, + /(@|@this\.|@[a-zA-Z_? \-0-9]+\.)([a-zA-Z_?\-0-9]+)((:|=|:=|=:=)([a-zA-Z,_().@?+\-*/ 0-9()]*))?\s/, (state, match, start, end) => { const docTitle = match[1].substring(1).replace(/\.$/, ''); const fieldKey = match[2]; @@ -334,18 +326,14 @@ export class RichTextRules { if (value?.includes(',') && !value.startsWith('((')) { const values = value.split(','); const strs = values.some(v => !v.match(/^[-]?[0-9.]$/)); - this.Document[DocData][fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v))); + this.Document['$' + fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v))); } else if (value) { Doc.SetField( this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, - assign.includes(':=') - ? undefined - : (gptval: FieldResult) => { - (dataDoc ? this.Document[DocData] : this.Document)[fieldKey] = gptval as string; - } + assign.includes(':=') ? undefined : (gptval: FieldResult) => (this.Document[(dataDoc ? '$' : '_') + fieldKey] = gptval as string) ); if (fieldKey === this.TextBox.fieldKey) return this.TextBox.EditorView!.state.tr; } @@ -399,11 +387,11 @@ export class RichTextRules { new InputRule(/#(@?[a-zA-Z_-]+[a-zA-Z_\-0-9]*)\s$/, (state, match, start, end) => { const tag = match[1]; if (!tag) return state.tr; - // this.Document[DocData]['#' + tag] = '#' + tag; - const tags = StrListCast(this.Document[DocData].tags); + // this.Document[['$#' + tag] = '#' + tag; + const tags = StrListCast(this.Document.$tags); if (!tags.includes(tag)) { tags.push(tag); - this.Document[DocData].tags = new List<string>(tags); + this.Document.$tags = new List<string>(tags); this.Document._layout_showTags = true; } const fieldView = state.schema.nodes.dashField.create({ fieldKey: tag.startsWith('@') ? tag.replace(/^@/, '') : '#' + tag }); @@ -416,22 +404,6 @@ export class RichTextRules { // # heading textblockTypeInputRule(/^(#{1,6})\s$/, schema.nodes.heading, match => ({ level: match[1].length })), - // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode) - new InputRule(/[ti!x]$/, (state, match, start, end) => { - if (state.selection.to === state.selection.from || !this.EnteringStyle) return null; - - const tag = match[0] === 't' ? 'todo' : match[0] === 'i' ? 'ignore' : match[0] === 'x' ? 'disagree' : match[0] === '!' ? 'important' : '??'; - const node = state.doc.resolve(start).nodeAfter; - - if (node?.marks.findIndex(m => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag); - return node - ? state.tr - .removeMark(start, end, schema.marks.user_mark) - .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })) - .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) - : state.tr; - }), - new InputRule(/%\(/, (state, match, start, end) => { const node = state.doc.resolve(start).nodeAfter; const sm = state.storedMarks?.slice() || []; diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx index 238267f6e..eeb604b57 100644 --- a/src/client/views/nodes/formattedText/SummaryView.tsx +++ b/src/client/views/nodes/formattedText/SummaryView.tsx @@ -1,12 +1,11 @@ import { TextSelection } from 'prosemirror-state'; -import { Fragment, Node, Slice } from 'prosemirror-model'; +import { Attrs, Fragment, Node, Slice } from 'prosemirror-model'; import * as ReactDOM from 'react-dom/client'; import * as React from 'react'; +import { EditorView } from 'prosemirror-view'; -interface ISummaryView {} // currently nothing needs to be rendered for the internal view of a summary. -// eslint-disable-next-line react/prefer-stateless-function -export class SummaryViewInternal extends React.Component<ISummaryView> { +export class SummaryViewInternal extends React.Component<object> { render() { return null; } @@ -18,30 +17,30 @@ export class SummaryViewInternal extends React.Component<ISummaryView> { // method instead of changing prosemirror's text when the expand/elide buttons are clicked. export class SummaryView { dom: HTMLSpanElement; // container for label and value - root: any; + root: ReactDOM.Root; - constructor(node: any, view: any, getPos: any) { + constructor(node: Node, view: EditorView, getPos: () => number | undefined) { this.dom = document.createElement('span'); this.dom.className = this.className(node.attrs.visibility); - this.dom.onpointerdown = (e: any) => { + this.dom.onpointerdown = (e: PointerEvent) => { this.onPointerDown(e, node, view, getPos); }; - this.dom.onkeypress = function (e: any) { + this.dom.onkeypress = function (e: KeyboardEvent) { e.stopPropagation(); }; - this.dom.onkeydown = function (e: any) { + this.dom.onkeydown = function (e: KeyboardEvent) { e.stopPropagation(); }; - this.dom.onkeyup = function (e: any) { + this.dom.onkeyup = function (e: KeyboardEvent) { e.stopPropagation(); }; - this.dom.onmousedown = function (e: any) { + this.dom.onmousedown = function (e: MouseEvent) { e.stopPropagation(); }; const js = node.toJSON; - node.toJSON = function (...args: any[]) { - return js.apply(this, args); + node.toJSON = function (...args: unknown[]) { + return js.apply(this, args as []); }; this.root = ReactDOM.createRoot(this.dom); @@ -54,18 +53,21 @@ export class SummaryView { } selectNode() {} - updateSummarizedText(start: any, view: any) { + updateSummarizedText(start: number, view: EditorView) { const mtype = view.state.schema.marks.summarize; const mtypeInc = view.state.schema.marks.summarizeInclusive; let endPos = start; const visited = new Set(); - for (let i: number = start + 1; i < view.state.doc.nodeSize - 1; i++) { + const summarized = new Set(); + const isSummary = (node: Node) => summarized.has(node) || node.marks.find(m => m.type === mtype || m.type === mtypeInc); + for (let i = start + 1; i < view.state.doc.nodeSize - 1; i++) { let skip = false; // eslint-disable-next-line no-loop-func view.state.doc.nodesBetween(start, i, (node: Node /* , pos: number, parent: Node, index: number */) => { + isSummary(node) && Array.from(node.children).forEach(child => summarized.add(child)); if (node.isLeaf && !visited.has(node) && !skip) { - if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) { + if (summarized.has(node) || isSummary(node)) { visited.add(node); endPos = i + node.nodeSize - 1; } else skip = true; @@ -75,21 +77,18 @@ export class SummaryView { return TextSelection.create(view.state.doc, start, endPos); } - onPointerDown = (e: any, node: any, view: any, getPos: any) => { + onPointerDown = (e: PointerEvent, node: Node, view: EditorView, getPos: () => number | undefined) => { const visible = !node.attrs.visibility; - const attrs = { ...node.attrs, visibility: visible }; - let textSelection = TextSelection.create(view.state.doc, getPos() + 1); - if (!visible) { - // update summarized text and save in attrs - textSelection = this.updateSummarizedText(getPos() + 1, view); - attrs.text = textSelection.content(); - attrs.textslice = attrs.text.toJSON(); - } + const textSelection = visible // + ? TextSelection.create(view.state.doc, (getPos() ?? 0) + 1) + : this.updateSummarizedText((getPos() ?? 0) + 1, view); // update summarized text and save in attrs + const text = textSelection.content(); + const attrs = { ...node.attrs, visibility: visible, ...(!visible ? { text, textslice: text.toJSON() } : {}) } as Attrs; view.dispatch( view.state.tr .setSelection(textSelection) // select the current summarized text (or where it will be if its collapsed) .replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text) // collapse/expand it - .setNodeMarkup(getPos(), undefined, attrs) + .setNodeMarkup(getPos() ?? 0, undefined, attrs) ); // update the attrs e.preventDefault(); e.stopPropagation(); diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index ba8e4faed..b7dae1ca3 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -118,6 +118,7 @@ export const marks: { [index: string]: MarkSpec } = { { tag: 'span', getAttrs: dom => { + if (!dom.style.fontSize) return false; return { fontSize: dom.style.fontSize ? dom.style.fontSize.toString() : '' }; }, }, @@ -132,16 +133,9 @@ export const marks: { [index: string]: MarkSpec } = { { tag: 'span', getAttrs: dom => { - const cstyle = getComputedStyle(dom); - if (cstyle.font) { - if (cstyle.font.indexOf('Times New Roman') !== -1) return { fontFamily: 'Times New Roman' }; - if (cstyle.font.indexOf('Arial') !== -1) return { fontFamily: 'Arial' }; - if (cstyle.font.indexOf('Georgia') !== -1) return { fontFamily: 'Georgia' }; - if (cstyle.font.indexOf('Comic Sans') !== -1) return { fontFamily: 'Comic Sans MS' }; - if (cstyle.font.indexOf('Tahoma') !== -1) return { fontFamily: 'Tahoma' }; - if (cstyle.font.indexOf('Crimson') !== -1) return { fontFamily: 'Crimson Text' }; - } - return { fontFamily: '' }; + const cstyle = dom.style.fontFamily; + if (!cstyle) return false; + return { fontFamily: cstyle }; }, }, ], @@ -155,6 +149,7 @@ export const marks: { [index: string]: MarkSpec } = { { tag: 'span', getAttrs: dom => { + if (!dom.style.color) return false; return { color: dom.getAttribute('color') }; }, }, @@ -171,6 +166,7 @@ export const marks: { [index: string]: MarkSpec } = { { tag: 'span', getAttrs: dom => { + if (!dom.getAttribute('background-color')) return false; return { fontHighlight: dom.getAttribute('background-color') }; }, }, @@ -239,13 +235,7 @@ export const marks: { [index: string]: MarkSpec } = { { tag: 'span', getAttrs: p => { - if (typeof p !== 'string') { - const style = getComputedStyle(p); - if (style.textDecoration === 'underline') return null; - if (p.parentElement?.outerHTML.indexOf('text-decoration: underline') !== -1 && p.parentElement?.outerHTML.indexOf('text-decoration-style: solid') !== -1) { - return null; - } - } + if (p.getAttribute('data-summarizeInclusive')) return [[{ style: 'data-summarizeInclusive: true' }]]; return false; }, }, @@ -255,7 +245,8 @@ export const marks: { [index: string]: MarkSpec } = { return [ 'span', { - style: 'text-decoration: underline; text-decoration-style: solid; text-decoration-color: rgba(204, 206, 210, 0.92)', + 'data-summarizeInclusive': 'true', + style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210)', }, ]; }, @@ -316,9 +307,9 @@ export const marks: { [index: string]: MarkSpec } = { attrs: { selected: { default: false }, }, - parseDOM: [{ style: 'background: yellow' }], + parseDOM: [{ style: 'background: lightGray' }], toDOM: node => { - return ['span', { style: `background: ${node.attrs.selected ? 'orange' : 'yellow'}` }]; + return ['span', { style: `background: ${node.attrs.selected ? 'orange' : 'lightGray'}` }]; }, }, diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 02ded3103..5d34afc8a 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -353,6 +353,7 @@ export const nodes: { [index: string]: NodeSpec } = { hard_break: { inline: true, group: 'inline', + marks: '_', selectable: false, parseDOM: [{ tag: 'br' }], toDOM() { @@ -386,10 +387,6 @@ export const nodes: { [index: string]: NodeSpec } = { }, }, { - style: 'list-style-type=disc', - getAttrs: () => ({ mapStyle: 'bullet' }), - }, - { tag: 'ol', getAttrs: dom => { return { @@ -443,6 +440,7 @@ export const nodes: { [index: string]: NodeSpec } = { mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet" visibility: { default: true }, }, + marks: '_', content: '(paragraph|audiotag)+ | ((paragraph|audiotag)+ ordered_list)', parseDOM: [ { diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx index 657e689bb..85bd95d15 100644 --- a/src/client/views/nodes/imageEditor/ImageEditor.tsx +++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx @@ -24,8 +24,8 @@ import { PointerHandler } from './imageEditorUtils/PointerHandler'; import { activeColor, bgColor, brushWidthOffset, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './imageEditorUtils/imageEditorConstants'; import { CutMode, CursorData, ImageDimensions, ImageEditTool, ImageToolType, Point } from './imageEditorUtils/imageEditorInterfaces'; import { DocumentView } from '../DocumentView'; -import { DocData } from '../../../../fields/DocSymbols'; import { SettingsManager } from '../../../util/SettingsManager'; +import { Upload } from '../../../../server/SharedMediaTypes'; interface GenerativeFillProps { imageEditorOpen: boolean; @@ -397,9 +397,8 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc const newImgDoc = await createNewImgDoc(finalImg, firstDoc); if (newImgDoc) { // set the image to transparent to remove the background / brushstrokes - const docData = newImgDoc[DocData]; - docData.backgroundColor = 'transparent'; - docData.disableMixBlend = true; + newImgDoc.$backgroundColor = 'transparent'; + newImgDoc.$disableMixBlend = true; if (firstDoc) setIsFirstDoc(false); setEdits([...prevEdits, { url: finalImgURL, saveRes: undefined }]); } @@ -476,7 +475,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc const createNewImgDoc = async (img: HTMLImageElement, firstDoc: boolean /*, parent?: Doc */): Promise<Doc | undefined> => { if (!imageRootDoc) return undefined; const { src } = img; - const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [src] }); + const [result] = (await Networking.PostToServer('/uploadRemoteImage', { sources: [src] })) as Upload.ImageInformation[]; const source = ClientUtils.prepend(result.accessPaths.agnostic.client); if (firstDoc) { diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/ImageMeshTool.ts b/src/client/views/nodes/imageEditor/imageMeshTool/ImageMeshTool.ts new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageMeshTool/ImageMeshTool.ts diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.scss b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.scss new file mode 100644 index 000000000..253f48f77 --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.scss @@ -0,0 +1,24 @@ +/* MeshTransformGrid.scss */ +.meshTransformGrid { + position: relative; + width: 100%; + height: 100%; + pointer-events: none; /* Prevents interaction with the grid itself */ + opacity: 5%; +} + +.grid-line { + position: absolute; + background-color: rgba(255, 255, 255, 0.6); /* Light grid lines */ +} + +.control-point { + position: absolute; + width: 12px; + height: 12px; + background-color: rgba(255, 255, 255, 1); /* White control points */ + border-radius: 50%; + cursor: pointer; + z-index: 10; + pointer-events: auto; /* Allows dragging of control points */ +} diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.tsx new file mode 100644 index 000000000..ee5c597e9 --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.tsx @@ -0,0 +1,109 @@ +import React, { useState, useEffect } from 'react'; +import './MeshTransformGrid.scss'; + +interface MeshTransformGridProps { + imageRef: React.RefObject<HTMLImageElement>; // Reference to the image element + gridXSize: number; // Number of X subdivisions + gridYSize: number; // Number of Y subdivisions + isInteractive: boolean; // Whether control points are interactive (can be dragged) +} + +const MeshTransformGrid: React.FC<MeshTransformGridProps> = ({ imageRef, gridXSize, gridYSize, isInteractive }) => { + const [controlPoints, setControlPoints] = useState<any[]>([]); + + // Set up control points based on image size and grid sizes + useEffect(() => { + if (imageRef.current) { + const { width, height, left, top } = imageRef.current.getBoundingClientRect(); + const newControlPoints = []; + + for (let i = 0; i <= gridYSize; i++) { + for (let j = 0; j <= gridXSize; j++) { + newControlPoints.push({ + id: `${i}-${j}`, + x: (j * width) / gridXSize + left, + y: (i * height) / gridYSize + top, + }); + } + } + + setControlPoints(newControlPoints); + } + }, [imageRef, gridXSize, gridYSize]); + + // Handle dragging of control points + const handleDrag = (e: React.MouseEvent, pointId: string) => { + if (!isInteractive) return; // Prevent dragging if grid is not interactive + + const { clientX, clientY } = e; + const updatedPoints = controlPoints.map((point) => { + if (point.id === pointId) { + return { ...point, x: clientX, y: clientY }; + } + return point; + }); + setControlPoints(updatedPoints); + }; + + // Render grid lines between control points + const renderGridLines = () => { + const lines = []; + for (let i = 0; i < controlPoints.length; i++) { + const point = controlPoints[i]; + const nextPoint = controlPoints[i + 1]; + + // Horizontal lines + if (nextPoint && i % (gridXSize + 1) !== gridXSize) { + lines.push({ + start: { x: point.x, y: point.y }, + end: { x: nextPoint.x, y: nextPoint.y }, + }); + } + + // Vertical lines + if (i + gridXSize + 1 < controlPoints.length) { + const downPoint = controlPoints[i + gridXSize + 1]; + lines.push({ + start: { x: point.x, y: point.y }, + end: { x: downPoint.x, y: downPoint.y }, + }); + } + } + return lines.map((line, index) => ( + <div + key={index} + className="grid-line" + style={{ + position: 'absolute', + left: `${line.start.x}px`, + top: `${line.start.y}px`, + width: `${Math.abs(line.end.x - line.start.x)}px`, + height: `${Math.abs(line.end.y - line.start.y)}px`, + border: '1px solid rgba(255, 255, 255, 0.6)', + }} + /> + )); + }; + + return ( + <div className="meshTransformGrid"> + {renderGridLines()} + + {controlPoints.map((point) => ( + <div + key={point.id} + className="control-point" + style={{ + left: `${point.x}px`, + top: `${point.y}px`, + transform: 'translate(-50%, -50%)', + }} + draggable={isInteractive} // Only allow dragging if interactive + onDrag={(e) => handleDrag(e, point.id)} + /> + ))} + </div> + ); +}; + +export default MeshTransformGrid; diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.scss b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.scss new file mode 100644 index 000000000..a72b2de6a --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.scss @@ -0,0 +1,21 @@ +/* MeshTransformButton.scss */ +.meshTransformBtnContainer { + position: relative; + width: 100%; + height: 100%; +} + +.grid-line { + position: absolute; + background-color: rgba(255, 255, 255, 0.6); +} + +.control-point { + position: absolute; + width: 12px; + height: 12px; + background-color: rgba(255, 255, 255, 1); + border-radius: 50%; + cursor: pointer; + z-index: 10; +} diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx new file mode 100644 index 000000000..eb68410b0 --- /dev/null +++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx @@ -0,0 +1,90 @@ +import './MeshTransformButton.scss'; +import * as React from 'react'; +import ReactLoading from 'react-loading'; +import { Button, IconButton, Type } from '@dash/components'; +import { AiOutlineInfo } from 'react-icons/ai'; +import { SettingsManager } from '../../../../util/SettingsManager'; +import MeshTransformGrid from './imageMesh'; + +interface ButtonContainerProps { + onClick: () => Promise<void>; + loading: boolean; + onReset: () => void; + btnText: string; + imageWidth: number; + imageHeight: number; + gridXSize: number; // X subdivisions + gridYSize: number; // Y subdivisions +} + +export function MeshTransformButton({ + loading, + onClick: startMeshTransform, + onReset, + btnText, + imageWidth, + imageHeight, + gridXSize, + gridYSize +}: ButtonContainerProps) { + const [showGrid, setShowGrid] = React.useState(false); + const [isGridInteractive, setIsGridInteractive] = React.useState(false); // Controls the dragging of control points + const imageRef = React.useRef<HTMLImageElement>(null); // Reference to the image element + + const handleGridToggle = () => { + if (showGrid) { + setShowGrid(false); // Hide the grid + setIsGridInteractive(false); // Disable control points manipulation + } else { + setShowGrid(true); // Show the grid + setIsGridInteractive(true); // Enable control points manipulation + } + }; + + return ( + <div className="meshTransformBtnContainer"> + <Button text="RESET" type={Type.PRIM} color={SettingsManager.userVariantColor} onClick={onReset} /> + {loading ? ( + <Button + text={btnText} + type={Type.TERT} + color={SettingsManager.userVariantColor} + icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />} + iconPlacement="right" + onClick={() => { + if (!loading) handleGridToggle(); // Toggle the grid visibility and control points manipulation + }} + /> + ) : ( + <Button + text={btnText} + type={Type.TERT} + color={SettingsManager.userVariantColor} + onClick={() => { + if (!loading) handleGridToggle(); // Toggle the grid visibility and control points manipulation + }} + /> + )} + + {/* The IconButton will toggle the grid */} + <IconButton + type={Type.SEC} + color={SettingsManager.userVariantColor} + tooltip="Toggle Grid" + icon={<AiOutlineInfo size="16px" />} + onClick={handleGridToggle} // Toggle the grid when clicked + /> + + {/* Only show the grid if `showGrid` is true */} + {showGrid && ( + <MeshTransformGrid + imageRef={imageRef} + gridXSize={gridXSize} + gridYSize={gridYSize} + isInteractive={isGridInteractive} // Pass the interactive flag to control point manipulation + /> + )} + <img ref={imageRef} src="your-image-source.jpg" alt="Mesh" style={{ width: imageWidth, height: imageHeight }} /> + </div> + ); +} diff --git a/src/client/views/nodes/importBox/ImportElementBox.tsx b/src/client/views/nodes/importBox/ImportElementBox.tsx index 317719032..7e470a642 100644 --- a/src/client/views/nodes/importBox/ImportElementBox.tsx +++ b/src/client/views/nodes/importBox/ImportElementBox.tsx @@ -22,9 +22,7 @@ export class ImportElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <div style={{ backgroundColor: 'pink' }}> <DocumentView - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} // - LayoutTemplateString={undefined} Document={this.Document} isContentActive={returnFalse} addDocument={returnFalse} diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 9ab5fb1bd..11f35b8ef 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -149,7 +149,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { setIsLoading = action((input?: boolean) => { this._isLoading = !!input; }); // prettier-ignore setShowAIGalleryVisibilty = action((visible: boolean) => { this._showAIGallery = visible; }); // prettier-ignore setBezierControlPoints = action((newPoints: { p1: number[]; p2: number[] }) => { - this.setEaseFunc(this.activeItem, `cubic-bezier(${newPoints.p1[0]}, ${newPoints.p1[1]}, ${newPoints.p2[0]}, ${newPoints.p2[1]})`); + this.activeItem && this.setEaseFunc(this.activeItem, `cubic-bezier(${newPoints.p1[0]}, ${newPoints.p1[1]}, ${newPoints.p2[0]}, ${newPoints.p2[1]})`); }); @computed get showEaseFunctions() { @@ -158,7 +158,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get currCPoints() { - return EaseFuncToPoints(this.activeItem.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease'); + return EaseFuncToPoints(this.activeItem?.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease'); } @computed @@ -175,7 +175,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return DocListCast(this.Document[this.presFieldKey]); } @computed get tagDocs() { - return this.childDocs.map(doc => Cast(doc.presentation_targetDoc, Doc, null)); + return this.childDocs.map(doc => DocCast(doc.presentation_targetDoc)!).filter(doc => doc); } @computed get itemIndex() { return NumCast(this.Document._itemIndex); @@ -187,11 +187,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return DocCast(this.activeItem?.presentation_targetDoc); } public static targetRenderedDoc = (doc: Doc) => { - const targetDoc = Cast(doc?.presentation_targetDoc, Doc, null); + const targetDoc = DocCast(doc?.presentation_targetDoc); return targetDoc?.layout_unrendered ? DocCast(targetDoc.annotationOn) : targetDoc; }; @computed get scrollable() { - if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._type_collection === CollectionViewType.Stacking) return true; + if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc?.type as DocumentType) || this.targetDoc?._type_collection === CollectionViewType.Stacking) return true; return false; } @computed get selectedDocumentView() { @@ -210,6 +210,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } componentDidMount() { + this._props.setContentViewBox?.(this); this._disposers.pause = reaction( () => SnappingManager.UserPanned, () => this.pauseAutoPres() @@ -272,8 +273,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; stopTempMedia = (targetDocField: FieldResult) => { - const targetDoc = DocCast(DocCast(targetDocField).annotationOn) ?? DocCast(targetDocField); - if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as DocumentType)) { + const targetDoc = DocCast(DocCast(targetDocField)?.annotationOn) ?? DocCast(targetDocField); + if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc?.type as DocumentType)) { const targMedia = DocumentView.getDocumentView(targetDoc); targMedia?.ComponentView?.Pause?.(); } @@ -294,13 +295,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.setIsLoading(true); const slideDefaults: { [key: string]: FieldResult } = { presentation_transition: 500, config_zoom: 1 }; const currSlideProperties = gptSlideProperties.reduce( - (prev, key) => { prev[key] = Field.toString(this.activeItem[key]) ?? prev[key]; return prev; }, + (prev, key) => { prev[key] = Field.toString(this.activeItem?.[key]) ?? prev[key]; return prev; }, slideDefaults); // prettier-ignore gptTrailSlideCustomization(input, JSON.stringify(currSlideProperties)) .then(res => (Object.entries(JSON.parse(res)) as string[][]).forEach(([key, val]) => { - this.activeItem[key] = (+val).toString() === val ? +val : (val ?? this.activeItem[key]); + this.activeItem && (this.activeItem[key] = (+val).toString() === val ? +val : (val ?? this.activeItem[key])); }) ) .catch(e => console.error(e)) @@ -324,7 +325,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const serial = nextSelected + 1 < this.childDocs.length && NumCast(this.childDocs[nextSelected + 1].presentation_groupWithUp) > 1; if (serial) { this.gotoDocument(nextSelected, this.activeItem, true, async () => { - const waitTime = NumCast(this.activeItem.presentation_duration); + const waitTime = NumCast(this.activeItem?.presentation_duration); await new Promise<void>(res => { setTimeout(res, Math.max(0, waitTime)); }); @@ -345,7 +346,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { progressivizedItems = (doc: Doc) => { const targetList = PresBox.targetRenderedDoc(doc); if (doc.presentation_indexed !== undefined && targetList) { - const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '_annotations']); + const listItems = (Cast(targetList[Doc.LayoutDataKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutDataKey(targetList) + '_annotations']); return listItems.filter(ldoc => !ldoc.layout_unrendered); } return undefined; @@ -363,7 +364,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { next = () => { const progressiveReveal = (first: boolean) => { const presIndexed = Cast(this.activeItem?.presentation_indexed, 'number', null); - if (presIndexed !== undefined) { + if (presIndexed !== undefined && this.activeItem) { const listItems = this.progressivizedItems(this.activeItem); const listItemDoc = listItems?.[presIndexed]; if (listItems && listItemDoc) { @@ -394,7 +395,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.childDocs[this.itemIndex + 1] !== undefined) { // Case 1: No more frames in current doc and next slide is defined, therefore move to next slide const slides = DocListCast(this.Document[StrCast(this.presFieldKey, 'data')]); - const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d)))) : this.itemIndex; + const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d) ?? d))) : this.itemIndex; // before moving onto next slide, run the subroutines :) const currentDoc = this.childDocs[this.itemIndex]; @@ -421,7 +422,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const { activeItem } = this; let prevSelected = this.itemIndex; // Functionality for group with up - let didZoom = activeItem.presentation_movement; + let didZoom = activeItem?.presentation_movement; for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].presentation_groupWithUp; prevSelected--) { didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presentation_movement : didZoom; } @@ -451,7 +452,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.stopTempMedia(from.presentation_targetDoc); } // If next slide is audio / video 'Play automatically' then the next slide should be played - if (this.layoutDoc.presentation_status !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.presentation_mediaStart === 'auto') { + if (this.layoutDoc.presentation_status !== PresStatus.Edit && (this.targetDoc?.type === DocumentType.AUDIO || this.targetDoc?.type === DocumentType.VID) && this.activeItem?.presentation_mediaStart === 'auto') { this.startTempMedia(this.targetDoc, this.activeItem); } if (!group) this.clearSelectedArray(); @@ -515,8 +516,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const activeFrame = activeItem.config_activeFrame ?? activeItem.config_currentFrame; if (activeFrame !== undefined) { const frameTime = NumCast(activeItem.presentation_transition, 500); - const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem.presentation_targetDoc).embedContainer) : DocCast(activeItem.presentation_targetDoc); - const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext; + const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem?.presentation_targetDoc)?.embedContainer) : DocCast(activeItem.presentation_targetDoc); + const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext)?.annotationOn) : acontext; if (context) { const ffview = CollectionFreeFormView.from(DocumentView.getFirstDocumentView(context)); if (ffview?.childDocs) { @@ -527,15 +528,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if ((pinDataTypes?.dataview && activeItem.config_data !== undefined) || (!pinDataTypes && activeItem.config_data !== undefined)) { bestTarget._dataTransition = `all ${transTime}ms`; - const fkey = Doc.LayoutFieldKey(bestTarget); + const fkey = Doc.LayoutDataKey(bestTarget); const setData = bestTargetView?.ComponentView?.setData; if (setData) setData(activeItem.config_data); else { - const bestTargetData = bestTarget[DocData]; - const current = bestTargetData[fkey]; - const hash = bestTargetData[fkey] ? stringHash(Field.toString(bestTargetData[fkey] as FieldType)) : undefined; - if (hash) bestTargetData[fkey + '_' + hash] = current instanceof ObjectField ? current[Copy]() : current; - bestTargetData[fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data; + const current = bestTarget['$' + fkey]; + const hash = bestTarget['$' + fkey] ? stringHash(Field.toString(bestTarget['$' + fkey] as FieldType)) : undefined; + if (hash) bestTarget['$' + fkey + '_' + hash] = current instanceof ObjectField ? current[Copy]() : current; + bestTarget['$' + fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data; } bestTarget[fkey + '_usePath'] = activeItem.config_usePath; setTimeout(() => { @@ -553,7 +553,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.config_clipWidth !== undefined)) { - const fkey = '_' + Doc.LayoutFieldKey(bestTarget); + const fkey = '_' + Doc.LayoutDataKey(bestTarget); if (bestTarget[fkey + '_clipWidth'] !== activeItem.config_clipWidth) { bestTarget[fkey + '_clipWidth'] = activeItem.config_clipWidth; changed = true; @@ -595,11 +595,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.config_fillColor !== undefined || activeItem.color !== undefined))) { if (bestTarget.fillColor !== activeItem.config_fillColor) { - bestTarget[DocData].fillColor = StrCast(activeItem.config_fillColor, StrCast(bestTarget.fillColor)); + bestTarget.$fillColor = StrCast(activeItem.config_fillColor, StrCast(bestTarget.fillColor)); changed = true; } if (bestTarget.color !== activeItem.config_color) { - bestTarget[DocData].color = StrCast(activeItem.config_color, StrCast(bestTarget.color)); + bestTarget.$color = StrCast(activeItem.config_color, StrCast(bestTarget.color)); changed = true; } if (bestTarget.width !== activeItem.width) { @@ -655,7 +655,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.config_annotations !== undefined)) { - const fkey = Doc.LayoutFieldKey(bestTarget); + const fkey = Doc.LayoutDataKey(bestTarget); const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.layout_unrendered); const newItems = DocListCast(activeItem.config_annotations).map(doc => { doc.hidden = false; @@ -668,11 +668,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return doc; }); const newList = new List<Doc>([...oldItems, ...hiddenItems, ...newItems]); - bestTarget[DocData][fkey + '_annotations'] = newList; + bestTarget['$' + fkey + '_annotations'] = newList; } if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.config_pinLayoutData !== undefined)) { changed = true; - const layoutField = Doc.LayoutFieldKey(bestTarget); + const layoutField = Doc.LayoutDataKey(bestTarget); const transitioned = new Set<Doc>(); StrListCast(activeItem.config_pinLayoutData) .map(str => JSON.parse(str) as { id: string; x: number; y: number; back: string; fill: string; w: number; h: number; data: string; text: string }) @@ -689,8 +689,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { data.fill && (doc._fillColor = data.fill); doc._width = data.w; doc._height = data.h; - data.data && (doc[DocData].data = field); - data.text && (doc[DocData].text = tfield); + data.data && (doc.$data = field); + data.text && (doc.$text = tfield); Doc.AddDocToList(bestTarget[DocData], layoutField, doc); } }); @@ -740,7 +740,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const { activeItem, targetDoc } = this; const finished = (options: FocusViewOptions) => { afterNav?.(options); - targetDoc[Animation] = undefined; + targetDoc && (targetDoc[Animation] = undefined); }; const selViewCache = Array.from(this.selectedArray); const dragViewCache = Array.from(this._dragArray); @@ -756,7 +756,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } finished(options); }); - PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection); + targetDoc && activeItem && PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection); }; static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: (options: FocusViewOptions) => void) { @@ -806,42 +806,44 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action doHideBeforeAfter = () => { this.childDocs.forEach((doc, index) => { - const curDoc = Cast(doc, Doc, null); - const tagDoc = PresBox.targetRenderedDoc(curDoc); - const itemIndexes = this.getAllIndexes(this.tagDocs, curDoc); - let opacity = index === this.itemIndex ? 1 : undefined; - if (curDoc.presentation_hide) { - if (index !== this.itemIndex) { - opacity = 1; + const curDoc = DocCast(doc); + if (curDoc) { + const tagDoc = PresBox.targetRenderedDoc(curDoc) ?? curDoc; + const itemIndexes = this.getAllIndexes(this.tagDocs, curDoc); + let opacity = index === this.itemIndex ? 1 : undefined; + if (curDoc.presentation_hide) { + if (index !== this.itemIndex) { + opacity = 1; + } } - } - const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement(); - if (curDoc.presentation_hideBefore && index === hidingIndBef) { - if (index > this.itemIndex) { - opacity = 0; - } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) { - opacity = 1; + const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement(); + if (curDoc.presentation_hideBefore && index === hidingIndBef) { + if (index > this.itemIndex) { + opacity = 0; + } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) { + opacity = 1; + } } - } - const hidingIndAft = - itemIndexes - .slice() - .reverse() - .find(item => item <= this.itemIndex) ?? itemIndexes.lastElement(); - if (curDoc.presentation_hideAfter && index === hidingIndAft) { - if (index < this.itemIndex) { - opacity = 0; - } else if (index === this.itemIndex || !curDoc.presentation_hideBefore) { - opacity = 1; + const hidingIndAft = + itemIndexes + .slice() + .reverse() + .find(item => item <= this.itemIndex) ?? itemIndexes.lastElement(); + if (curDoc.presentation_hideAfter && index === hidingIndAft) { + if (index < this.itemIndex) { + opacity = 0; + } else if (index === this.itemIndex || !curDoc.presentation_hideBefore) { + opacity = 1; + } } - } - const hidingInd = itemIndexes.find(item => item === this.itemIndex); - if (curDoc.presentation_hide && index === hidingInd) { - if (index === this.itemIndex) { - opacity = 0; + const hidingInd = itemIndexes.find(item => item === this.itemIndex); + if (curDoc.presentation_hide && index === hidingInd) { + if (index === this.itemIndex) { + opacity = 0; + } } + opacity !== undefined && (tagDoc.opacity = opacity === 1 ? undefined : opacity); } - opacity !== undefined && (tagDoc.opacity = opacity === 1 ? undefined : opacity); }); }; @@ -940,7 +942,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { initializePresState = (startIndex: number) => { this.childDocs.forEach((doc, index) => { - const tagDoc = PresBox.targetRenderedDoc(doc); + const tagDoc = PresBox.targetRenderedDoc(doc) ?? doc; if (doc.presentation_hideBefore && index > startIndex) tagDoc.opacity = 0; if (doc.presentation_hideAfter && index < startIndex) tagDoc.opacity = 0; if (doc.presentation_indexed !== undefined && index >= startIndex) { @@ -969,7 +971,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.layoutDoc.presentation_status = PresStatus.Autoplay; this.initializePresState(startIndex); const func = () => { - const delay = NumCast(this.activeItem.presentation_duration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presentation_transition); + const delay = NumCast(this.activeItem?.presentation_duration, this.activeItem?.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem?.presentation_transition); this._presTimer = setTimeout(() => { if (this.next() === false) this.layoutDoc.presentation_status = this._exitTrail?.() ?? PresStatus.Manual; this.layoutDoc.presentation_status === PresStatus.Autoplay && func(); @@ -1060,7 +1062,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return !results.some(r => !r); }; - childLayoutTemplate = () => Docs.Create.PresElementBoxDocument(); + childLayoutTemplate = () => Docs.Create.PresSlideDocument(); removeDocument = (doc: Doc | Doc[]) => !toList(doc) .map(d => Doc.RemoveDocFromList(this.Document, this.fieldKey, d)) @@ -1077,13 +1079,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { */ @computed get listOfSelected() { return Array.from(this.selectedArray).map((doc, index) => { - const curDoc = Cast(doc, Doc, null); + const curDoc = DocCast(doc); + if (!curDoc) return null; const tagDoc = Cast(curDoc.presentation_targetDoc, Doc, null); if (curDoc && curDoc === this.activeItem) return ( <div key={doc[Id]} className="selectedList-items"> <b> - {index + 1}. {StrCast(curDoc.title)}) + {index + 1}. {StrCast(curDoc.title)} </b> </div> ); @@ -1277,7 +1280,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const presCollection = collection; const dv = DocumentView.getDocumentView(presCollection); this.childDocs.forEach((doc, index) => { - const tagDoc = PresBox.targetRenderedDoc(doc); + const tagDoc = PresBox.targetRenderedDoc(doc) ?? doc; const srcContext = Cast(tagDoc.embedContainer, Doc, null); const labelCreator = (top: number, left: number, edge: number, fontSize: number) => ( <div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top, left, width: edge, height: edge, fontSize }} onClick={() => this.selectElement(doc)}> @@ -1526,17 +1529,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Applies the slide transiiton settings to all docs in the array @undoBatch applyTo = (array: Doc[]) => { - this.updateMovement(this.activeItem.presentation_movement as PresMovement, true); - this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true); - this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true); - this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true); - const { presentation_transition: pt, presentation_duration: pd, presentation_hideBefore: ph, presentation_hideAfter: pa } = this.activeItem; - array.forEach(curDoc => { - curDoc.presentation_transition = pt; - curDoc.presentation_duration = pd; - curDoc.presentation_hideBefore = ph; - curDoc.presentation_hideAfter = pa; - }); + if (this.activeItem) { + this.updateMovement(this.activeItem.presentation_movement as PresMovement, true); + this.updateEffect(this.activeItem.presentation_effect as PresEffect, false, true); + this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true); + this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true); + const { presentation_transition: pt, presentation_duration: pd, presentation_hideBefore: ph, presentation_hideAfter: pa } = this.activeItem; + array.forEach(curDoc => { + curDoc.presentation_transition = pt; + curDoc.presentation_duration = pd; + curDoc.presentation_hideBefore = ph; + curDoc.presentation_hideAfter = pa; + }); + } }; @computed get visibilityDurationDropdown() { @@ -1673,15 +1678,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onClick={() => { activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined; activeItem.presentation_hideBefore = activeItem.presentation_indexed !== undefined; - const tagDoc = PresBox.targetRenderedDoc(this.activeItem); + const tagDoc = PresBox.targetRenderedDoc(activeItem) ?? activeItem; const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type; activeItem.presentation_indexedStart = type === DocumentType.COL ? 1 : 0; // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized. // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list. - let dataField = Doc.LayoutFieldKey(tagDoc); + let dataField = Doc.LayoutDataKey(tagDoc); if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField += '_annotations'; - if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`); + if (DocCast(activeItem.presentation_targetDoc)?.annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`); else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`); }}> Enable @@ -1797,12 +1802,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onClick={() => { this.updateEffect(elem.effect, false); this.updateEffectDirection(elem.direction); - this.updateEffectTiming(this.activeItem, { - type: SpringType.CUSTOM, - stiffness: elem.stiffness, - damping: elem.damping, - mass: elem.mass, - }); + this.activeItem && + this.updateEffectTiming(this.activeItem, { + type: SpringType.CUSTOM, + stiffness: elem.stiffness, + damping: elem.damping, + mass: elem.mass, + }); }}> <SlideEffect dir={elem.direction} presEffect={elem.effect} springSettings={elem} infinite> <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[i] }} /> @@ -1947,8 +1953,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { formLabelPlacement="left" closeOnSelect items={easeItems} - selectedVal={this.activeItem.presentation_easeFunc ? (StrCast(this.activeItem.presentation_easeFunc).startsWith('cubic') ? 'custom' : StrCast(this.activeItem.presentation_easeFunc)) : 'ease'} - setSelectedVal={val => typeof val === 'string' && this.setEaseFunc(this.activeItem, val !== 'custom' ? val : TIMING_DEFAULT_MAPPINGS.ease)} + selectedVal={this.activeItem?.presentation_easeFunc ? (StrCast(this.activeItem.presentation_easeFunc).startsWith('cubic') ? 'custom' : StrCast(this.activeItem.presentation_easeFunc)) : 'ease'} + setSelectedVal={val => typeof val === 'string' && this.activeItem && this.setEaseFunc(this.activeItem, val !== 'custom' ? val : TIMING_DEFAULT_MAPPINGS.ease)} dropdownType={DropdownType.SELECT} type={Type.TERT} /> @@ -2145,9 +2151,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get mediaOptionsDropdown() { const { activeItem } = this; if (activeItem && this.targetDoc) { - const renderTarget = PresBox.targetRenderedDoc(this.activeItem); + const renderTarget = PresBox.targetRenderedDoc(this.activeItem ?? this.targetDoc) ?? this.targetDoc; const clipStart = NumCast(renderTarget.clipStart); - const clipEnd = NumCast(renderTarget.clipEnd, clipStart + NumCast(renderTarget[Doc.LayoutFieldKey(renderTarget) + '_duration'])); + const clipEnd = NumCast(renderTarget.clipEnd, clipStart + NumCast(renderTarget[Doc.LayoutDataKey(renderTarget) + '_duration'])); const configClipEnd = NumCast(activeItem.config_clipEnd) < NumCast(activeItem.config_clipStart) ? clipEnd - clipStart : NumCast(activeItem.config_clipEnd); return ( <div className="presBox-ribbon" onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> @@ -2699,7 +2705,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && - (this.activeItem.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); + (this.activeItem?.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0; const inOverlay = Doc.IsInMyOverlay(this.Document); // Case 1: There are still other frames and should go through all frames before going to next slide @@ -2892,7 +2898,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && - (this.activeItem.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); + (this.activeItem?.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0; return this._props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player <div @@ -2984,16 +2990,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /> ) : null} </div> - {/* { - // if the document type is a presentation, then the collection stacking view has a "+ new slide" button at the bottom of the stack - <Tooltip title={<div className="dash-tooltip">{'Click on document to pin to presentaiton or make a marquee selection to pin your desired view'}</div>}> - <button className="add-slide-button" onPointerDown={this.startMarqueeCreateSlide}> - + NEW SLIDE - </button> - </Tooltip> - } */} </div> - {/* presbox chatbox */} {this._chatActive && <div className="presBox-chatbox" />} </div> ); diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresSlideBox.scss index 9ac2b5a94..9ac2b5a94 100644 --- a/src/client/views/nodes/trails/PresElementBox.scss +++ b/src/client/views/nodes/trails/PresSlideBox.scss diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresSlideBox.tsx index a76805960..3dbb3da88 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresSlideBox.tsx @@ -7,7 +7,7 @@ import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUt import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; -import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; @@ -25,16 +25,16 @@ import { returnEmptyDocViewList } from '../../StyleProvider'; import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { PresBox } from './PresBox'; -import './PresElementBox.scss'; +import './PresSlideBox.scss'; import { PresMovement } from './PresEnums'; /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. */ @observer -export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { +export class PresSlideBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(PresElementBox, fieldKey); + return FieldView.LayoutString(PresSlideBox, fieldKey); } private _itemRef: React.RefObject<HTMLDivElement> = React.createRef(); private _dragRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -52,7 +52,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // the presentation view that renders this slide @computed get presBoxView() { - return this.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as PresBox; + return this.DocumentView?.() + .containerViewPath?.() + .slice() + .reverse() + .find(dv => dv?.ComponentView instanceof PresBox)?.ComponentView as Opt<PresBox>; } // the presentation view document that renders this slide @@ -63,12 +67,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // Since this node is being rendered with a template, this method retrieves // the actual slide being rendered from the auto-generated rendering template @computed get slideDoc() { - return DocCast(this.Document.rootDocument, this.Document); + return this.rootDoc; } // this is the document in the workspaces that is targeted by the slide @computed get targetDoc() { - return Cast(this.slideDoc.presentation_targetDoc, Doc, null) || this.slideDoc; + return DocCast(this.slideDoc.presentation_targetDoc, this.slideDoc)!; } // computes index of this presentation slide in the presBox list @@ -114,7 +118,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return !this.slideDoc.presentation_expandInlineButton || !this.targetDoc ? null : ( <div className="presItem-embedded" style={{ height: this.embedHeight(), width: '50%' }}> <DocumentView - Document={PresBox.targetRenderedDoc(this.slideDoc)} + Document={PresBox.targetRenderedDoc(this.slideDoc) ?? this.slideDoc} PanelWidth={this.embedWidth} PanelHeight={this.embedHeight} isContentActive={this._props.isContentActive} @@ -235,10 +239,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { e.clientY, undefined, action(() => { - Array.from(classesToRestore).forEach(pair => { - // eslint-disable-next-line prefer-destructuring - pair[0].className = pair[1]; - }); + Array.from(classesToRestore).forEach(pair => (pair[0].className = pair[1])); this._dragging = false; }) ); @@ -348,7 +349,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { /// remove all videos that have been recorded from overlay (leave videso that are being recorded to avoid losing data) static removeEveryExistingRecordingInOverlay = () => { - Doc.MyOverlayDocs.filter(doc => doc.slides !== null && PresElementBox.videoIsRecorded(DocCast(doc.slides))) // + Doc.MyOverlayDocs.filter(doc => doc.slides !== null && PresSlideBox.videoIsRecorded(DocCast(doc.slides))) // .forEach(Doc.RemFromMyOverlay); }; @@ -363,9 +364,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { showRecording = undoable( action((activeItem: Doc, iconClick: boolean = false) => { // remove the overlays on switch *IF* not opened from the specific icon - if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay(); + if (!iconClick) PresSlideBox.removeEveryExistingRecordingInOverlay(); - activeItem.recording && Doc.AddToMyOverlay(DocCast(activeItem.recording)); + DocCast(activeItem.recording) && Doc.AddToMyOverlay(DocCast(activeItem.recording)!); }), 'show video recording' ); @@ -373,7 +374,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { startRecording = undoable( action((e: React.MouseEvent, activeItem: Doc) => { e.stopPropagation(); - if (PresElementBox.videoIsRecorded(activeItem)) { + if (PresSlideBox.videoIsRecorded(activeItem)) { // if we already have an existing recording this.showRecording(activeItem, true); // // if we already have an existing recording @@ -382,7 +383,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // we dont have any recording // Remove every recording that already exists in overlay view // this is a design decision to clear to focus in on the recoding mode - PresElementBox.removeEveryExistingRecordingInOverlay(); + PresSlideBox.removeEveryExistingRecordingInOverlay(); // create and add a recording to the slide // make recording box appear in the bottom right corner of the screen @@ -412,7 +413,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { TreeView.ToggleChildrenRun.get(this.slideDoc)?.(); // call this.slideDoc.recurChildren() to get all the children - // if (iconClick) PresElementBox.showVideo = false; + // if (iconClick) PresSlideBox.showVideo = false; }; @computed @@ -451,7 +452,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> ); items.push( - <Tooltip key="slash" title={<div className="dash-tooltip">{this.videoRecordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}> + <Tooltip key="slash" title={<div className="dash-tooltip">{this.videoRecordingIsInOverlay ? 'Hide Recording' : `${PresSlideBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}> <div className="slideButton" onClick={e => (this.videoRecordingIsInOverlay ? this.hideRecording(e) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> <FontAwesomeIcon icon={`video${this.videoRecordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} /> </div> @@ -621,7 +622,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } } -Docs.Prototypes.TemplateMap.set(DocumentType.PRESELEMENT, { - layout: { view: PresElementBox, dataField: 'data' }, - options: { acl: '', title: 'pres element template', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' }, +Docs.Prototypes.TemplateMap.set(DocumentType.PRESSLIDE, { + layout: { view: PresSlideBox, dataField: 'data' }, + options: { acl: '', title: 'presSlide', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true }, }); diff --git a/src/client/views/nodes/trails/index.ts b/src/client/views/nodes/trails/index.ts index 7b18974df..a5bc55221 100644 --- a/src/client/views/nodes/trails/index.ts +++ b/src/client/views/nodes/trails/index.ts @@ -1,3 +1,3 @@ export * from './PresBox'; -export * from './PresElementBox'; +export * from './PresSlideBox'; export * from './PresEnums'; |
