aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r--src/client/views/nodes/AudioBox.tsx4
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx23
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx139
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx48
-rw-r--r--src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx6
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx472
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx33
-rw-r--r--src/client/views/nodes/DataVizBox/components/PieChart.tsx16
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx17
-rw-r--r--src/client/views/nodes/DiagramBox.tsx17
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx105
-rw-r--r--src/client/views/nodes/DocumentView.scss13
-rw-r--r--src/client/views/nodes/DocumentView.tsx334
-rw-r--r--src/client/views/nodes/EquationBox.tsx1
-rw-r--r--src/client/views/nodes/FieldView.tsx30
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx47
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx11
-rw-r--r--src/client/views/nodes/IconTagBox.scss1
-rw-r--r--src/client/views/nodes/IconTagBox.tsx73
-rw-r--r--src/client/views/nodes/ImageBox.scss41
-rw-r--r--src/client/views/nodes/ImageBox.tsx754
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx68
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx38
-rw-r--r--src/client/views/nodes/LabelBox.tsx35
-rw-r--r--src/client/views/nodes/LinkBox.tsx7
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.tsx12
-rw-r--r--src/client/views/nodes/MapBox/AnimationUtility.ts2
-rw-r--r--src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx2
-rw-r--r--src/client/views/nodes/MapBox/MapAnchorMenu.tsx45
-rw-r--r--src/client/views/nodes/MapBox/MapBox.scss8
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx178
-rw-r--r--src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx5
-rw-r--r--src/client/views/nodes/PDFBox.scss6
-rw-r--r--src/client/views/nodes/PDFBox.tsx32
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx3
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx8
-rw-r--r--src/client/views/nodes/ScriptingBox.tsx13
-rw-r--r--src/client/views/nodes/VideoBox.tsx48
-rw-r--r--src/client/views/nodes/WebBox.tsx273
-rw-r--r--src/client/views/nodes/WebBoxRenderer.js2
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.tsx25
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx4
-rw-r--r--src/client/views/nodes/chatbot/tools/GetDocsTool.ts5
-rw-r--r--src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts7
-rw-r--r--src/client/views/nodes/formattedText/DailyJournal.tsx280
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.scss4
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx7
-rw-r--r--src/client/views/nodes/formattedText/EquationView.tsx5
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss5
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx559
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx24
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts209
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx26
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts77
-rw-r--r--src/client/views/nodes/formattedText/SummaryView.tsx51
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts31
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts6
-rw-r--r--src/client/views/nodes/imageEditor/ImageEditor.tsx14
-rw-r--r--src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts6
-rw-r--r--src/client/views/nodes/imageEditor/imageMeshTool/ImageMeshTool.ts0
-rw-r--r--src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.scss24
-rw-r--r--src/client/views/nodes/imageEditor/imageMeshTool/imageMesh.tsx109
-rw-r--r--src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.scss21
-rw-r--r--src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx81
-rw-r--r--src/client/views/nodes/importBox/ImportElementBox.tsx2
-rw-r--r--src/client/views/nodes/scrapbook/EmbeddedDocView.tsx52
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookBox.tsx143
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookContent.tsx23
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlot.scss85
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlot.tsx28
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts25
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx206
-rw-r--r--src/client/views/nodes/trails/PresSlideBox.scss (renamed from src/client/views/nodes/trails/PresElementBox.scss)0
-rw-r--r--src/client/views/nodes/trails/PresSlideBox.tsx (renamed from src/client/views/nodes/trails/PresElementBox.tsx)44
-rw-r--r--src/client/views/nodes/trails/index.ts2
75 files changed, 3061 insertions, 2099 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..3805b0dca 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;
}
@@ -147,7 +148,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
public static getValues(doc: Doc, time: number, fillIn: boolean = true) {
return CollectionFreeFormDocumentView.animFields.reduce(
(p, val) => {
- p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce(
+ p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : [])!.reduce(
(prev, v, i) => ((i <= Math.round(time) && v !== undefined) || prev === undefined ? v : prev),
undefined as unknown as number
);
@@ -160,7 +161,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
public static getStringValues(doc: Doc, time: number) {
return CollectionFreeFormDocumentView.animStringFields.reduce(
(p, val) => {
- p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((prev, v, i) => ((i <= Math.round(time) && v !== undefined) || prev === undefined ? v : prev), undefined as unknown as string);
+ p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])])!.reduce((prev, v, i) => ((i <= Math.round(time) && v !== undefined) || prev === undefined ? v : prev), undefined as unknown as string);
return p;
},
{} as { [val: string]: Opt<string> }
@@ -170,7 +171,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
public static setStringValues(time: number, d: Doc, vals: { [val: string]: Opt<string> }) {
const timecode = Math.round(time);
Object.keys(vals).forEach(val => {
- const findexed = Cast(d[`${val}_indexed`], listSpec('string'), []).slice();
+ const findexed = Cast(d[`${val}_indexed`], listSpec('string'), [])!.slice();
findexed[timecode] = vals[val] || '';
d[`${val}_indexed`] = new List<string>(findexed);
});
@@ -179,14 +180,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) {
const timecode = Math.round(time);
Object.keys(vals).forEach(val => {
- const findexed = Cast(d[`${val}_indexed`], listSpec('number'), []).slice();
+ const findexed = Cast(d[`${val}_indexed`], listSpec('number'), [])!.slice();
findexed[timecode] = vals[val] as unknown as number;
d[`${val}_indexed`] = new List<number>(findexed);
});
}
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;
@@ -203,15 +204,15 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
docs.forEach(doc => {
this.animFields.forEach(val => {
const findexed = Cast(doc[`${val.key}_indexed`], listSpec('number'), null);
- findexed?.length <= timecode + 1 && findexed.push(undefined as unknown as number);
+ (findexed?.length ?? 0) <= timecode + 1 && findexed?.push(undefined as unknown as number);
});
this.animStringFields.forEach(val => {
const findexed = Cast(doc[`${val}_indexed`], listSpec('string'), null);
- findexed?.length <= timecode + 1 && findexed.push(undefined as unknown as string);
+ (findexed?.length ?? 0) <= timecode + 1 && findexed?.push(undefined as unknown as string);
});
this.animDataFields(doc).forEach(val => {
const findexed = Cast(doc[`${val}_indexed`], listSpec(InkField), null);
- findexed?.length <= timecode + 1 && findexed.push(undefined as unknown as InkField);
+ (findexed?.length ?? 0) <= timecode + 1 && findexed?.push(undefined as unknown as InkField);
});
});
return newTimer;
@@ -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..9369ff98a 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',
@@ -112,7 +108,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// all CSV records in the dataset (that aren't an empty row)
@computed.struct get records() {
try {
- const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
+ const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '');
this._urlError = false;
return records?.filter(record => Object.keys(record).some(key => record[key])) ?? [];
} catch {
@@ -348,7 +344,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
componentDidMount() {
this._props.setContentViewBox?.(this);
if (!this._urlError) {
- if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData();
+ if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '')) this.fetchData();
}
this._disposers.datavis = reaction(
() => {
@@ -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)
@@ -390,8 +386,8 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.layoutDoc._dataViz_schemaOG = loading;
}
const ogDoc = this.layoutDoc._dataViz_schemaOG as Doc;
- const ogHref = CsvCast(ogDoc[this.fieldKey]) ? CsvCast(ogDoc[this.fieldKey]).url.href : undefined;
- const { href } = CsvCast(this.Document[this.fieldKey]).url;
+ const ogHref = CsvCast(ogDoc[this.fieldKey]) ? CsvCast(ogDoc[this.fieldKey])!.url.href : undefined;
+ const { href } = CsvCast(this.Document[this.fieldKey])?.url ?? { href: '' };
if (ogHref && !DataVizBox.datasetSchemaOG.has(href)) {
// sets original dataset to the var
const lastRow = current.pop();
@@ -403,7 +399,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
},
current => {
if (current) {
- const { href } = CsvCast(this.Document[this.fieldKey]).url;
+ const { href } = CsvCast(this.Document[this.fieldKey])?.url ?? { href: '' };
if (this.layoutDoc.dataViz_schemaLive) DataVizBox.dataset.set(href, current);
else DataVizBox.dataset.set(href, DataVizBox.datasetSchemaOG.get(href)!);
}
@@ -418,9 +414,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
fetchData = () => {
if (!this.Document.dataViz_asSchema) {
- DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests
+ DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '', []); // assign temporary dataset as a lock to prevent duplicate server requests
fetch('/csvData?uri=' + (this.dataUrl?.url.href ?? '')) //
- .then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, jsonRes))));
+ .then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '', jsonRes))));
}
};
@@ -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
@@ -527,7 +523,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
GPTPopup.Instance.createFilteredDoc = this.createFilteredDoc;
GPTPopup.Instance.setDataJson('');
GPTPopup.Instance.setMode(GPTPopupMode.DATA);
- const csvdata = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
+ const csvdata = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '');
GPTPopup.Instance.setDataJson(JSON.stringify(csvdata));
GPTPopup.Instance.generateDataAnalysis();
});
@@ -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/SchemaCSVPopUp.tsx b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
index 8ae29a88c..1e2a95d31 100644
--- a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
+++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx
@@ -11,10 +11,8 @@ import { DragManager } from '../../../util/DragManager';
import { DocumentView } from '../DocumentView';
import './SchemaCSVPopUp.scss';
-interface SchemaCSVPopUpProps {}
-
@observer
-export class SchemaCSVPopUp extends React.Component<SchemaCSVPopUpProps> {
+export class SchemaCSVPopUp extends React.Component<object> {
// eslint-disable-next-line no-use-before-define
static Instance: SchemaCSVPopUp;
@@ -23,7 +21,7 @@ export class SchemaCSVPopUp extends React.Component<SchemaCSVPopUpProps> {
@observable public target: Doc | undefined = undefined;
@observable public visible: boolean = false;
- constructor(props: SchemaCSVPopUpProps) {
+ constructor(props: object) {
super(props);
makeObservable(this);
SchemaCSVPopUp.Instance = this;
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index 5a9442d2f..a7c4a00b0 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -1,27 +1,28 @@
-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 { List } from '../../../../../fields/List';
-import { listSpec } from '../../../../../fields/Schema';
-import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
+import { 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 +40,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 StrCast(this.layoutDoc.dataViz_histogram_defaultColor, '#69b3a2')!;
+ }
+ @computed get barColors() {
+ return StrListCast(this.layoutDoc.dataViz_histogram_barColors);
+ }
+ @computed get selectedBins() {
+ return NumListCast(this.layoutDoc.dataViz_histogram_selectedBins);
}
@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 +126,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 +158,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 +171,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 +277,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 +293,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 +308,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 +340,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 +358,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 +381,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 +440,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 +460,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}
/>
&nbsp;
@@ -564,7 +472,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..80fadf178 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -22,11 +22,11 @@ export interface SelectedDataPoint extends DataPoint {
}
export interface LineChartProps {
vizBox: DataVizBox;
- Document: Doc;
+ Doc: Doc;
layoutDoc: Doc;
axes: string[];
titleCol: string;
- records: { [key: string]: any }[];
+ records: { [key: string]: string }[];
width: number;
height: number;
dataDoc: Doc;
@@ -47,13 +47,13 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
@observable _currSelected: DataPoint | undefined = undefined;
// TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates
- constructor(props: any) {
+ constructor(props: LineChartProps) {
super(props);
makeObservable(this);
}
@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,12 +74,12 @@ 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
// otherwise, use the selection of whatever is linked to us
- const incomingVizBox = DocumentView.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox;
+ const incomingVizBox = this.parentViz && (DocumentView.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox);
const highlitedRowIds = NumListCast(incomingVizBox?.layoutDoc?.dataViz_highlitedRows);
return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor
}
@@ -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;
};
@@ -170,8 +170,8 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
}
});
if (!ptWasSelected) {
- selectedDatapoints.push(d.x + ',' + d.y);
- this._currSelected = selectedDatapoints.length > 1 ? undefined : d;
+ selectedDatapoints?.push(d.x + ',' + d.y);
+ this._currSelected = (selectedDatapoints?.length ?? 0 > 1) ? undefined : d;
}
// for filtering child dataviz docs
@@ -190,7 +190,14 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
}
}
- drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>, higlightFocusPt: any, tooltip: any) {
+ drawDataPoints(
+ data: DataPoint[], //
+ idx: number,
+ xScale: d3.ScaleLinear<number, number, never>,
+ yScale: d3.ScaleLinear<number, number, never>,
+ higlightFocusPt: d3.Selection<SVGGElement, unknown, null, undefined>,
+ tooltip: d3.Selection<HTMLDivElement, unknown, null, undefined>
+ ) {
if (this._lineChartSvg) {
const circleClass = '.circle-' + idx;
this._lineChartSvg
@@ -211,7 +218,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
.on('mouseleave', () => {
tooltip?.transition().duration(300).style('opacity', 0);
})
- .on('click', (e: any) => {
+ .on('click', e => {
const d0 = { x: Number(e.target.getAttribute('data-x')), y: Number(e.target.getAttribute('data-y')) };
// find .circle-d1 with data-x = d0.x and data-y = d0.y
this.setCurrSelected(d0);
@@ -220,7 +227,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
}
}
- drawChart = (dataSet: any[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => {
+ drawChart = (dataSet: { x: number; y: number }[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => {
// clearing tooltip and the current chart
d3.select(this._lineChartRef).select('svg').remove();
d3.select(this._lineChartRef).select('.tooltip').remove();
@@ -277,7 +284,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
svg.append('path').attr('stroke', 'red');
// legend
- const color: any = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]);
+ const color = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]);
svg.selectAll('mydots')
.data([this._props.axes[1], this._props.axes[2]])
.enter()
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..ad2731109 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() {
@@ -129,7 +128,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
selected.splice(selected.indexOf(rowId), 1);
} else selected?.push(rowId);
e.stopPropagation();
- this.hasRowsToFilter = selected.length > 0;
+ this.hasRowsToFilter = (selected?.length ?? 0) > 0;
};
columnPointerDown = (e: React.PointerEvent, col: string) => {
@@ -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..7cfccf0dc 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',
@@ -186,11 +185,6 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return '( )';
};
- /**
- * This stops scroll wheel events when they are used to scroll the face collection.
- */
- onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
-
render() {
return (
<div
@@ -198,12 +192,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
style={{
pointerEvents: this._props.isContentActive() ? undefined : 'none',
}}
- ref={action((ele: HTMLDivElement | null) => {
- this._boxRef?.removeEventListener('wheel', this.onPassiveWheel);
- this._boxRef = ele;
- // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
- ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
- })}>
+ ref={r => this.fixWheelEvents(r, this._props.isContentActive)}>
<div className="DIYNodeBox-searchbar">
<input type="text" value={this._inputValue} onKeyDown={action(e => e.key === 'Enter' && this.generateMermaidCode())} onChange={action(e => (this._inputValue = e.target.value))} />
<button type="button" onClick={this.generateMermaidCode}>
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..c4351a200 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -119,6 +119,7 @@
display: flex;
justify-content: center;
align-items: center;
+ margin: auto;
position: relative; // allows contents to be positioned relative/below title
> .formattedTextBox {
position: absolute; // position a child text box
@@ -273,7 +274,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 {
@@ -294,6 +303,6 @@
background: transparent;
.documentView-editorView-resizer {
- height: 5px;
+ height: 2px;
}
}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index cac276535..05706fe6b 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',
@@ -687,14 +648,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
rootSelected = () => this._rootSelected;
panelHeight = () => this._props.PanelHeight() - this.headerMargin - 2 * NumCast(this.Document.borderWidth);
- screenToLocalContent = () =>
- 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;
+ aiShift = () => (!this.viewingAiEditor() ? 0 : (this._props.PanelWidth() - this.aiContentsWidth()) / 2);
+ aiScale = () => (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)];
@@ -717,14 +675,14 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
return this._props.styleProvider?.(doc, props, property);
};
- @observable _aiWinHeight = 88;
+ @observable _aiWinHeight = 32;
- 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 +691,56 @@ 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);
+ aiContentsHeight = () => Math.max(10, this._props.PanelHeight() - (this._aiWinHeight + (this.tagsOverlayFunc() ? 22 : 0)) * this.uiBtnScaling);
+ @computed get aiEditor() {
+ return (
+ <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);
+ @observable _contentsRef: DocumentContentsView | undefined = undefined;
@computed get viewBoxContents() {
TraceMobx();
const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -753,63 +751,28 @@ 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={action((r: DocumentContentsView) => (this._contentsRef = r))}
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}
rootSelected={this.rootSelected}
onClickScript={this.onClickFunc}
setTitleFocus={this.setTitleFocus}
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 +790,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 +804,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 +820,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;
@@ -934,8 +897,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
}}>
<FormattedTextBox
{...this._props}
- yPadding={10}
- xPadding={10}
+ yMargin={10}
+ xMargin={10}
fieldKey={this.showCaption}
styleProvider={this.captionStyleProvider}
dontRegisterView
@@ -1043,7 +1006,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 +1121,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 +1139,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 +1157,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;
+ 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 +1192,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 +1246,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() {
@@ -1290,7 +1275,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
if (this.ComponentView?.screenBounds?.()) {
return this.ComponentView.screenBounds();
}
- const xf = this.screenToContentsTransform().scale(this.nativeScaling).inverse();
+ const xf = this.screenToContentBoundsTransform().inverse();
const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)];
// transition is returned so that the bounds will 'update' at the end of an animated transition. This is needed by xAnchor in LinkBox
@@ -1327,10 +1312,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 +1325,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 +1345,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:
}
@@ -1404,32 +1389,36 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined);
}, 'set custom view');
- public static setDefaultTemplate(checkResult?: boolean) {
- if (checkResult) {
- return Doc.UserDoc().defaultTextLayout;
+ private static getTemplate(view: DocumentView | undefined) {
+ if (view) {
+ if (!view.layoutDoc.isTemplateDoc) {
+ MakeTemplate(view.Document);
+ Doc.AddDocToList(Doc.UserDoc(), 'template_user', view.Document);
+ Doc.AddDocToList(DocListCast(Doc.MyTools?.data)[1], 'data', makeUserTemplateButtonOrImage(view.Document));
+ DocCast(Doc.UserDoc().template_user) && view.Document && Doc.AddDocToList(DocCast(Doc.UserDoc().template_user)!, 'data', view.Document);
+ return view.Document;
+ }
+ return DocCast(Doc.LayoutField(view.Document)) ?? view.Document;
}
+ }
+ public static setDefaultTemplate(checkResult?: boolean) {
+ if (checkResult) return Doc.UserDoc().defaultTextLayout;
const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined;
undoable(() => {
- let tempDoc: Opt<Doc>;
- if (view) {
- if (!view.layoutDoc.isTemplateDoc) {
- 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);
- } else {
- tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]);
- if (!tempDoc) {
- tempDoc = view.Document;
- while (tempDoc && !Doc.isTemplateDoc(tempDoc)) tempDoc = DocCast(tempDoc.proto);
- }
- }
- }
+ const tempDoc = DocumentView.getTemplate(view);
Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined;
}, 'set default template')();
return undefined;
}
+ public static setDefaultImageTemplate(checkResult?: boolean) {
+ if (checkResult) return Doc.UserDoc().defaultImageLayout;
+ const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined;
+ undoable(() => {
+ const tempDoc = DocumentView.getTemplate(view);
+ Doc.UserDoc().defaultImageLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined;
+ }, 'set default image template')();
+ return undefined;
+ }
/**
* This switches between the current view of a Doc and a specified alternate layout view.
@@ -1445,10 +1434,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 +1457,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.
*/
@@ -1501,17 +1490,22 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
isHovering = () => this._isHovering;
selfView = () => this;
/**
- * @returns Transform to the document view (in the coordinate system of whatever contains the DocumentView)
+ * @returns Transform to the document view's available panel space (in the coordinate system of whatever contains the DocumentView)
*/
screenToViewTransform = () => this._props.ScreenToLocalTransform();
/**
+ * @returns Transform to the document view after centering in available panel space(in the coordinate system of whatever contains the DocumentView)
+ */
+ private screenToContentBoundsTransform = () => this.screenToViewTransform().translate(-this.centeringX, -this.centeringY);
+ /**
* @returns Transform to the coordinate system of the contents of the document view (includes native dimension scaling and centering)
*/
screenToContentsTransform = () =>
this._props
.ScreenToLocalTransform()
.translate(-this.centeringX, -this.centeringY)
- .scale(1 / this.nativeScaling);
+ .translate(-(this._docViewInternal?.aiShift() ?? 0), 0)
+ .scale((this._docViewInternal?.aiScale() ?? 1) / this.nativeScaling);
htmlOverlay = () => {
const effect = StrCast(this._htmlOverlayEffect?.presentation_effect, StrCast(this._htmlOverlayEffect?.followLinkAnimEffect));
@@ -1521,7 +1515,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 +1542,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 +1571,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 +1615,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 +1694,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 +1717,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 +1737,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/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index dcc6e27ed..3cacb6692 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -1,7 +1,6 @@
import { action, computed, makeObservable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc } from '../../../fields/Doc';
import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DocUtils } from '../../documents/DocUtils';
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 2e40f39ed..f6b405a43 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -6,15 +6,17 @@ import { DateField } from '../../../fields/DateField';
import { Doc, Field, FieldType, Opt } from '../../../fields/Doc';
import { List } from '../../../fields/List';
import { ScriptField } from '../../../fields/ScriptField';
+import { WebField } from '../../../fields/URLField';
+import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
import { Transform } from '../../util/Transform';
+import { ContextMenuProps } from '../ContextMenuItem';
import { PinProps } from '../PinFuncs';
import { ViewBoxInterface } from '../ViewBoxInterface';
import { DocumentView } from './DocumentView';
import { FocusViewOptions } from './FocusViewOptions';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { OpenWhere } from './OpenWhere';
-import { WebField } from '../../../fields/URLField';
-import { ContextMenuProps } from '../ContextMenuItem';
export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>;
export type StyleProviderFuncType = (
@@ -47,13 +49,11 @@ 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)
- xPadding?: number;
- yPadding?: number;
+ screenXPadding?: (view: DocumentView | undefined) => number; // padding in screen space coordinates (used by text box to reflow around UI buttons in carouselView)
+ xMargin?: number;
+ yMargin?: number;
dontRegisterView?: boolean;
dropAction?: dropActionType;
dragAction?: dropActionType;
@@ -61,14 +61,15 @@ 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
isGroupActive?: () => string | undefined; // is this document part of a group that is active
// eslint-disable-next-line no-use-before-define
setContentViewBox?: (view: ViewBoxInterface<FieldViewProps>) => void; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox
-
+ rejectDrop?: (de: DragManager.DropEvent, subView?: DocumentView) => boolean; // whether a document drop is rejected
PanelWidth: () => number;
PanelHeight: () => number;
isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
@@ -79,14 +80,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 +101,6 @@ export interface FieldViewSharedProps {
waitForDoubleClickToClick?: () => 'never' | 'always' | undefined;
defaultDoubleClick?: () => 'default' | 'ignore' | undefined;
pointerEvents?: () => Opt<Property.PointerEvents>;
- suppressSetHeight?: boolean;
}
/**
@@ -110,13 +108,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..a3167ee06 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';
@@ -117,10 +117,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
default: type = 'slider';
break;
} // prettier-ignore
- const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, value, _readOnly_: value === undefined });
+ const numScript = (value?: number) => ScriptCast(this.Document.script)?.script.run({ this: this.Document, value, _readOnly_: value === undefined });
const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
// Script for checking the outcome of the toggle
- const checkResult = Number(Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)));
+ const checkResult = Number(Number(numScript()?.result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)));
return (
<NumberDropdown
@@ -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
/>
@@ -207,6 +207,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
*/
@computed get dropdownListButton() {
const script = ScriptCast(this.Document.script);
+ if (!script) return null;
const selectedFunc = () => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
const { buttonList, selectedVal, getStyle, jsx, toolTip } = (() => {
switch (this.Document.title) {
@@ -252,7 +253,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 +273,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 +290,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));
+ 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} />,
@@ -313,7 +317,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
// it would be better to pas the 'added' flag to the callback script, but our script generator from currentUserUtils makes it hard to define
// arbitrary parameter variables (but it could be done as a special case or with additional effort when creating the sript)
const itemsChanged = items.filter(item => (val instanceof Array ? val.includes(item.toolType as string | number) : item.toolType === val));
- itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false }));
+ itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick)?.script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false }));
}}
/>
);
@@ -339,11 +343,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 +409,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/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 91c351895..8e4b64851 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -2,10 +2,11 @@ import functionPlot, { Chart } from 'function-plot';
import { action, computed, makeObservable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
+import { Doc, DocListCast, NumListCast } from '../../../fields/Doc';
import { List } from '../../../fields/List';
-import { listSpec } from '../../../fields/Schema';
-import { Cast, StrCast } from '../../../fields/Types';
+import { StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DocUtils } from '../../documents/DocUtils';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -15,8 +16,6 @@ import { undoBatch } from '../../util/UndoManager';
import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { PinDocView, PinProps } from '../PinFuncs';
import { FieldView, FieldViewProps } from './FieldView';
-import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
-import { emptyFunction } from '../../../Utils';
@observer
export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -76,7 +75,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
this._plotEle = ele || this._plotEle;
const width = this._props.PanelWidth();
const height = this._props.PanelHeight();
- const xrange = Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]);
+ const xrange = NumListCast(this.layoutDoc.xRange, [-10, 10])!;
try {
this._plotEle?.children.length && this._plotEle.removeChild(this._plotEle.children[0]);
this._plot = functionPlot({
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..d04ec3a10 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,50 @@ 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 (
+ <div>
+ {' '}
+ {/* tooltips require the wrapped item to be an element ref */}
+ <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}
+ />
+ </div>
+ );
+ };
- 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 +107,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 3d6942e6f..5a6292fab 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 {
@@ -240,8 +236,39 @@
.imageBox-aiView-input {
overflow: hidden;
text-overflow: ellipsis;
- max-width: 65%;
+ max-width: 80%;
width: 100%;
color: black;
}
}
+.imageBox-regenerate-dialog {
+ position: absolute;
+ background: white;
+ padding: 10px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ z-index: 10000;
+
+ h3 {
+ margin-top: 0;
+ }
+
+ input {
+ width: 300px;
+ padding: 8px;
+ margin-bottom: 10px;
+ }
+
+ .buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+
+ .generate-btn {
+ background: #0078d4;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ }
+ }
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 5b06e9fc5..f7ad5c7e2 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,8 +1,8 @@
-import { Button, Colors, Size, Type } from '@dash/components';
+import { Button, Colors, EditableText, IconButton, NumberDropdown, Size, Toggle, ToggleType, Type } from '@dash/components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Slider, Tooltip } from '@mui/material';
+import { Tooltip } from '@mui/material';
import axios from 'axios';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { extname } from 'path';
import * as React from 'react';
@@ -13,11 +13,12 @@ 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';
@@ -26,7 +27,7 @@ import { Networking } from '../../Network';
import { DragManager } from '../../util/DragManager';
import { SettingsManager } from '../../util/SettingsManager';
import { SnappingManager } from '../../util/SnappingManager';
-import { undoable, undoBatch } from '../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
@@ -36,7 +37,6 @@ import { OverlayView } from '../OverlayView';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { PinDocView, PinProps } from '../PinFuncs';
import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
-import { FireflyImageData, isFireflyImageData } from '../smartdraw/FireflyConstants';
import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
import { StickerPalette } from '../smartdraw/StickerPalette';
import { StyleProp } from '../StyleProp';
@@ -45,8 +45,8 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FocusViewOptions } from './FocusViewOptions';
import './ImageBox.scss';
import { OpenWhere } from './OpenWhere';
-import { RichTextField } from '../../../fields/RichTextField';
+const DefaultPath = '/assets/unknown-file-icon-hi.png';
export class ImageEditorData {
// eslint-disable-next-line no-use-before-define
private static _instance: ImageEditorData;
@@ -101,7 +101,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable private _regenInput = '';
@observable private _canInteract = true;
@observable private _regenerateLoading = false;
- @observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : [];
+
+ // Add these observable properties to the ImageBox class
+ @observable private _outpaintingInProgress = false;
+ @observable private _outpaintingPrompt = '';
constructor(props: FieldViewProps) {
super(props);
@@ -146,11 +149,11 @@ 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), height: this.layoutDoc._height }),
({ nativeSize, width, height }) => {
- if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !height) {
+ if (!this.layoutDoc._layout_nativeDimEditable || !height || this.layoutDoc.layout_resetNativeDim) {
+ this.layoutDoc.layout_resetNativeDim = undefined; // template images need to reset their dimensions when they are rendered with content. afterwards, remove this flag.
this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
}
},
@@ -166,6 +169,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
},
{ fireImmediately: true }
);
+ this._disposers.outpaint = reaction(
+ () => this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined && !SnappingManager.ShiftKey,
+ complete => complete && this.openOutpaintPrompt(),
+ { fireImmediately: true }
+ );
}
componentWillUnmount() {
@@ -200,7 +208,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
drop = undoable(
action((e: Event, de: DragManager.DropEvent) => {
- if (de.complete.docDragData) {
+ if (de.complete.docDragData && !this._props.rejectDrop?.(de, this.DocumentView?.())) {
let added: boolean | undefined;
const hitDropTarget = (ele: HTMLElement, dropTarget: HTMLDivElement | null): boolean => {
if (!ele) return false;
@@ -210,19 +218,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 oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title));
+ 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_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;
@@ -252,17 +262,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;
}
@@ -284,40 +294,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;
+ const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw;
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);
@@ -339,6 +347,228 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
});
+ @observable _showOutpaintPrompt: boolean = false;
+ @observable _outpaintPromptInput: string = 'Extend this image naturally with matching content';
+
+ @action
+ openOutpaintPrompt = () => {
+ this._outpaintVAlign = '';
+ this._outpaintAlign = '';
+ this._showOutpaintPrompt = true;
+ };
+
+ @action
+ closeOutpaintPrompt = () => {
+ this._showOutpaintPrompt = false;
+ };
+
+ @action
+ cancelOutpaintPrompt = () => {
+ const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']);
+ const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']);
+ this.Document._width = origWidth;
+ this.Document._height = origHeight;
+ this._outpaintingInProgress = false;
+ this.closeOutpaintPrompt();
+ };
+
+ @action
+ handlePromptChange = (val: string | number) => {
+ this._outpaintPromptInput = '' + val;
+ };
+
+ @action
+ submitOutpaintPrompt = () => {
+ this.closeOutpaintPrompt();
+ this.processOutpaintingWithPrompt(this._outpaintPromptInput);
+ };
+
+ @action
+ processOutpaintingWithPrompt = async (customPrompt: string) => {
+ const field = Cast(this.dataDoc[this.fieldKey], ImageField);
+ if (!field) return;
+
+ // Set flag that outpainting is in progress
+ this._outpaintingInProgress = true;
+
+ // Revert dimensions if prompt is blank (acts like Cancel)
+ if (!customPrompt) {
+ this.cancelOutpaintPrompt();
+ return;
+ }
+
+ try {
+ const currentPath = this.choosePath(field.url);
+ const newWidth = NumCast(this.Document._width);
+ const newHeight = NumCast(this.Document._height);
+
+ // 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 origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']);
+ const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']);
+ const response = await Networking.PostToServer('/outpaintImage', {
+ imageUrl: currentPath,
+ prompt: customPrompt,
+ originalDimensions: { width: Math.min(newWidth, origWidth), height: Math.min(newHeight, origHeight) },
+ newDimensions: { width: newWidth, height: newHeight },
+ halignment: this._outpaintAlign,
+ valignment: this._outpaintVAlign,
+ });
+
+ const error = ('error' in response && (response.error as string)) || '';
+ if (error.includes('Dropbox') && confirm('Outpaint image failed. Try authorizing DropBox?\r\n' + error.replace(/^[^"]*/, ''))) {
+ DrawingFillHandler.authorizeDropbox();
+ } else {
+ const batch = UndoManager.StartBatch('outpaint image');
+ if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') {
+ 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),
+ });
+
+ 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;
+ this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined;
+ this.Document[this.fieldKey + '_outpaintOriginalHeight'] = undefined;
+ } else {
+ this.cancelOutpaintPrompt();
+ alert('Failed to receive a valid image URL from server.');
+ }
+ batch.end();
+ }
+
+ this._mainCont?.removeChild(loadingOverlay);
+ } catch (error) {
+ this.cancelOutpaintPrompt();
+ alert('An error occurred while outpainting.' + error);
+ } finally {
+ runInAction(() => (this._outpaintingInProgress = false));
+ }
+ };
+
+ @observable _outpaintAlign = '';
+ @observable _outpaintVAlign = '';
+ @computed get outpaintVertical() {
+ return this._props.PanelWidth() / this._props.PanelHeight() < this.nativeSize.nativeWidth / this.nativeSize.nativeHeight;
+ }
+
+ componentUI = (/* boundsLeft: number, boundsTop: number*/) =>
+ !this._showOutpaintPrompt ? null : (
+ <div
+ key="imageBox-componentui"
+ className="imageBox-regenerate-dialog"
+ style={{
+ top: -70 + (this._props.DocumentView?.().getBounds?.top ?? 0),
+ left: this._props.DocumentView?.().getBounds?.left ?? 0,
+ backgroundColor: SettingsManager.userBackgroundColor,
+ color: SettingsManager.userColor,
+ }}>
+ <div style={{ position: 'absolute', top: 5, right: 5 }}>
+ <IconButton type={Type.TERT} onClick={this.cancelOutpaintPrompt} icon={<FontAwesomeIcon icon="times" color={'red'} />} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} />
+ </div>
+ <div>Outpaint Image</div>
+ <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
+ <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" style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
+ <IconButton type={Type.TERT} onClick={this.submitOutpaintPrompt} icon={<AiOutlineSend />} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} />
+ <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
+ {this.outpaintVertical ? null : (
+ <Toggle
+ type={Type.TERT}
+ toggleType={ToggleType.BUTTON}
+ toggleStatus={this._outpaintAlign === 'left' && this._outpaintVAlign === ''}
+ onClick={action(() => (this._outpaintAlign = 'left'))}
+ icon={<FontAwesomeIcon icon="chevron-left" color={SnappingManager.userColor} />}
+ color={SnappingManager.userColor}
+ background={SnappingManager.userVariantColor}
+ />
+ )}
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+ {!this.outpaintVertical ? null : (
+ <Toggle
+ type={Type.TERT}
+ toggleType={ToggleType.BUTTON}
+ toggleStatus={this._outpaintAlign === '' && this._outpaintVAlign === 'top'}
+ onClick={action(() => (this._outpaintVAlign = 'top'))}
+ icon={<FontAwesomeIcon icon="chevron-up" color={SnappingManager.userColor} />}
+ color={SnappingManager.userColor}
+ background={SnappingManager.userColor}
+ />
+ )}
+ <Toggle
+ type={Type.TERT}
+ toggleType={ToggleType.BUTTON}
+ toggleStatus={this._outpaintAlign === '' && this._outpaintVAlign === ''}
+ onClick={action(() => {
+ this._outpaintAlign = '';
+ this._outpaintVAlign = '';
+ })}
+ icon={<FontAwesomeIcon icon="bullseye" color={SnappingManager.userColor} />}
+ color={SnappingManager.userColor}
+ background={SnappingManager.userColor}
+ />
+ {!this.outpaintVertical ? null : (
+ <Toggle
+ type={Type.TERT}
+ toggleType={ToggleType.BUTTON}
+ toggleStatus={this._outpaintAlign === '' && this._outpaintVAlign === 'bottom'}
+ onClick={action(() => (this._outpaintVAlign = 'bottom'))}
+ icon={<FontAwesomeIcon icon="chevron-down" color={SnappingManager.userColor} />}
+ color={SnappingManager.userColor}
+ background={SnappingManager.userColor}
+ />
+ )}
+ </div>
+ {this.outpaintVertical ? null : (
+ <Toggle
+ type={Type.TERT}
+ toggleType={ToggleType.BUTTON}
+ toggleStatus={this._outpaintAlign === 'right' && this._outpaintVAlign === ''}
+ onClick={action(() => (this._outpaintAlign = 'right'))}
+ icon={<FontAwesomeIcon icon="chevron-right" color={SnappingManager.userColor} />}
+ color={SnappingManager.userColor}
+ background={SnappingManager.userColor}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+
specificContextMenu = (): void => {
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
@@ -346,36 +576,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
- funcs.push({
- description: 'GetImageText',
- event: () => {
+ 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',
- });
- funcs.push({
- description: 'Expand Image',
- event: () => {
- Networking.PostToServer('/expandImage', {
- prompt: 'sunny skies',
- 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),
- }).then(res => {
- const info = res as Upload.ImageInformation;
- const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
- DocUtils.assignImageInfo(info, img);
- this._props.addDocTab(img, OpenWhere.addRight);
- });
- },
- icon: 'expand-arrows-alt',
- });
+ }); // prettier-ignore
funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' });
funcs.push({ description: 'Open Image Editor', event: this.docEditorView, icon: 'pencil-alt' });
this.layoutDoc.ai &&
@@ -396,20 +606,35 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')),
icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down',
});
+ // Add new outpainting option
+ 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({
+ description: 'View Original Image',
+ event: action(() => {
+ const alternates = DocListCast(this.dataDoc[this.fieldKey + '_alternates']);
+ if (alternates && alternates.length) {
+ // Toggle to show the original image
+ this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate';
+ }
+ }),
+ icon: 'image',
+ });
ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
}
};
// 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,
@@ -422,19 +647,18 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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);
};
- getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
+ getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc.freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
@computed get usingAlternate() {
const usePath = StrCast(this.Document[this.fieldKey + '_usePath']);
@@ -443,7 +667,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@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);
@@ -453,15 +677,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
/**
* 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`];
@@ -508,30 +732,33 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
@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_generatedDocs) && DocumentView.showDocument(DocCast(this.Document.ai_generatedDocs)!, { openLocation: OpenWhere.addRight })}
+ style={{
+ display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_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;
@@ -541,11 +768,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@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;
@@ -572,21 +803,33 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
})}
key={this.layoutDoc[Id]}
onPointerDown={this.marqueeDown}>
- <div className="imageBox-fader" style={{ opacity: backAlpha }}>
+ <div
+ className="imageBox-fader"
+ style={{
+ opacity: backAlpha,
+ flexDirection: this._outpaintVAlign ? 'row' : 'column',
+ alignItems: this._outpaintAlign === 'left' || this._outpaintVAlign === 'top' ? 'flex-start' : this._outpaintAlign === 'right' || this._outpaintVAlign === 'bottom' ? 'flex-end' : undefined,
+ }}>
<img
alt=""
ref={action((r: HTMLImageElement | null) => (this.imageRef = r))}
key="paths"
src={srcpath}
- style={{ transform, transformOrigin }}
- onError={action(e => {
- this._error = e.toString();
- })}
+ style={{
+ position: 'relative',
+ transform,
+ transformOrigin,
+ width: this._outpaintAlign ? 'max-content' : this._outpaintAlign ? '100%' : undefined,
+ height: this._outpaintVAlign ? 'max-content' : this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined ? '100%' : undefined,
+ }}
+ 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') }}>
+ <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>
)}
@@ -601,34 +844,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
@observable private _fireflyRefStrength = 0;
- componentAIViewHistory = () => (
- <div className="imageBox-aiView-history">
- <Button text="Clear History" type={Type.SEC} size={Size.XSMALL} />
- {this._prevImgs.map(img => (
- <div key={img.pathname}>
- <img
- className="imageBox-aiView-img"
- src={ClientUtils.prepend(img.pathname.replace(extname(img.pathname), '_s' + extname(img.pathname)))}
- onClick={() => {
- this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
- this.dataDoc.ai_firefly_prompt = img.prompt;
- this.dataDoc.ai_firefly_seed = img.seed;
- }}
- />
- <span>{img.prompt}</span>
- </div>
- ))}
- </div>
- );
-
componentAIView = () => {
- const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
return (
<div className="imageBox-aiView">
<div className="imageBox-aiView-regenerate">
- <span className="imageBox-aiView-firefly" style={{ color: SnappingManager.userColor }}>
- Firefly:
- </span>
<input
style={{ color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
className="imageBox-aiView-input"
@@ -642,57 +861,39 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<Button
text="Create"
type={Type.TERT}
+ size={Size.XSMALL}
color={SnappingManager.userColor}
background={SnappingManager.userBackgroundColor}
// style={{ alignSelf: 'flex-end' }}
icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
iconPlacement="right"
- onClick={action(async () => {
+ onClick={action(() => {
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)));
- } else {
- SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then(
- action(newImgs => {
- const firstImg = newImgs[0];
- if (isFireflyImageData(firstImg)) {
- 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.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;
- this.dataDoc[this.fieldKey] = imgField;
- this._regenerateLoading = false;
- this._regenInput = '';
- }
- })
- );
- }
+ DrawingFillHandler.drawingToImage(this.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title))?.then(action(() => (this._regenerateLoading = false)));
})}
/>
</div>
- </div>
- <div className="imageBox-aiView-strength">
- <span className="imageBox-aiView-similarity" style={{ color: SnappingManager.userColor }}>
- Similarity
- </span>
- <Slider
- className="imageBox-aiView-slider"
- sx={{
- '& .MuiSlider-track': { color: SettingsManager.userColor },
- '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
- '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
- }}
- min={0}
- max={100}
- step={1}
- size="small"
- value={this._fireflyRefStrength}
- onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
- valueLabelDisplay="auto"
- />
+ <div>
+ <NumberDropdown
+ color={SnappingManager.userColor}
+ background={SnappingManager.userBackgroundColor}
+ numberDropdownType="slider"
+ showPlusMinus={false}
+ formLabel="similarity"
+ tooltip="structure similarity of created images to current image"
+ type={Type.PRIM}
+ width={75}
+ min={0}
+ max={100}
+ number={this._fireflyRefStrength}
+ size={Size.XXSMALL}
+ setNumber={undoable(
+ action(val => this._canInteract && (this._fireflyRefStrength = val as number)),
+ `${this.Document.title} button set from list`
+ )}
+ fillWidth
+ />
+ </div>
</div>
</div>
);
@@ -704,21 +905,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
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
@@ -743,85 +947,91 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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
- className="imageBox"
- onContextMenu={this.specificContextMenu}
- ref={this.createDropTarget}
- onScroll={action(() => {
- if (!this._forcedScroll) {
- if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) {
- this._ignoreScroll = true;
- this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop;
- this._ignoreScroll = false;
+ <>
+ <div
+ className="imageBox"
+ onContextMenu={this.specificContextMenu}
+ ref={this.createDropTarget}
+ onScroll={action(() => {
+ if (!this._forcedScroll) {
+ if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) {
+ this._ignoreScroll = true;
+ this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop;
+ this._ignoreScroll = false;
+ }
}
- }
- })}
- style={{
- width: this._props.PanelWidth() ? undefined : `100%`,
- height: this._props.PanelHeight() ? undefined : `100%`,
- pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
- borderRadius,
- overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden',
- }}>
- <CollectionFreeFormView
- ref={this._ffref}
- {...this._props}
- Document={doc}
- setContentViewBox={emptyFunction}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- renderDepth={this._props.renderDepth + 1}
- fieldKey={this.annotationKey}
- styleProvider={this._props.styleProvider}
- isAnnotationOverlay
- annotationLayerHostsContent
- PanelWidth={this._props.PanelWidth}
- PanelHeight={this._props.PanelHeight}
- ScreenToLocalTransform={this.screenToLocalTransform}
- select={emptyFunction}
- focus={this.focus}
- getScrollHeight={this.getScrollHeight}
- NativeDimScaling={returnOne}
- isAnyChildContentActive={returnFalse}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.addDocument}>
- {this.content}
- </CollectionFreeFormView>
- {this.Loading ? (
- <div className="loading-spinner" style={{ position: 'absolute' }}>
- <ReactLoading type="spin" height={50} width={50} color={'blue'} />
- </div>
- ) : null}
- {this.regenerateImageIcon}
- {this.overlayImageIcon}
- {this.annotationLayer}
- {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : (
- <MarqueeAnnotator
+ })}
+ style={{
+ width: this._props.PanelWidth() ? undefined : `100%`,
+ height: this._props.PanelHeight() ? undefined : `100%`,
+ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
+ borderRadius,
+ overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden',
+ }}>
+ <CollectionFreeFormView
+ ref={this._ffref}
+ {...this._props}
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>
+ setContentViewBox={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ renderDepth={this._props.renderDepth + 1}
+ fieldKey={this.annotationKey}
+ styleProvider={this._props.styleProvider}
+ isAnnotationOverlay
+ annotationLayerHostsContent
+ PanelWidth={this._props.PanelWidth}
+ PanelHeight={this._props.PanelHeight}
+ ScreenToLocalTransform={this.screenToLocalTransform}
+ select={emptyFunction}
+ focus={this.focus}
+ rejectDrop={this._props.rejectDrop}
+ getScrollHeight={this.getScrollHeight}
+ NativeDimScaling={returnOne}
+ isAnyChildContentActive={returnFalse}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}>
+ {this.content}
+ </CollectionFreeFormView>
+ {this.Loading ? (
+ <div className="loading-spinner" style={{ position: 'absolute' }}>
+ <ReactLoading type="spin" height={50} width={50} color={'blue'} />
+ </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()}
+ />
+ )}
+ {this._outpaintingInProgress && (
+ <div className="imageBox-outpaintingSpinner">
+ <ReactLoading type="spin" color="#666" height={60} width={60} />
+ </div>
+ )}
+ </div>
+ </>
);
}
@@ -834,10 +1044,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const file = input.files?.[0];
if (file) {
const disposer = OverlayView.ShowSpinner();
- DocUtils.uploadFileToDoc(file, {}, this.Document).then(doc => {
- disposer();
- doc && (doc.height = undefined);
- });
+ const [{ result }] = await Networking.UploadFilesToServer({ file });
+ if (result instanceof Error) {
+ alert('Error uploading files - possibly due to unsupported file types');
+ } else {
+ this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
+ !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc);
+ }
+ disposer();
} else {
console.log('No file selected');
}
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 b08ed84b7..4cbe01b82 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -27,7 +27,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
private dropDisposer?: DragManager.DragDropDisposer;
private _timeout: NodeJS.Timeout | undefined;
private _divRef: HTMLDivElement | null = null;
- private _reaction: IReactionDisposer | undefined;
+ private _disposers: { [key: string]: IReactionDisposer } = {};
constructor(props: FieldViewProps) {
super(props);
@@ -43,7 +43,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
componentDidMount() {
this._props.setContentViewBox?.(this);
- this._reaction = reaction(
+ this._disposers.active = reaction(
() => this.Title,
() => document.activeElement !== this._divRef && this._forceRerender++
);
@@ -51,7 +51,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
componentWillUnMount() {
this._timeout && clearTimeout(this._timeout);
this.setText(this._divRef?.innerText ?? '');
- this._reaction?.();
+ Object.values(this._disposers).forEach(disposer => disposer());
}
@observable _forceRerender = 0;
@@ -171,20 +171,21 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
render() {
TraceMobx();
const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
+ const [xmargin, ymargin] = [NumCast(this.layoutDoc._xMargin), NumCast(this.layoutDoc._uMargin)];
return (
<div className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this.boxShadow }}>
<div
className="labelBox-mainButton"
style={{
backgroundColor: this.backgroundColor,
- color: StrCast(this.layoutDoc._text_fontColor, StrCast(this.layoutDoc._color)),
- fontFamily: StrCast(this.layoutDoc._text_fontFamily, StrCast(Doc.UserDoc().fontFamily)) || 'inherit',
+ color: StrCast(this.layoutDoc[`${this.fieldKey}_fontColor`], StrCast(this.layoutDoc._color)),
+ fontFamily: StrCast(this.layoutDoc[`${this.fieldKey}_fontFamily`], StrCast(Doc.UserDoc().fontFamily)) || 'inherit',
letterSpacing: StrCast(this.layoutDoc.letterSpacing),
- textTransform: StrCast(this.layoutDoc[this.fieldKey + '_transform']) as Property.TextTransform,
- paddingLeft: NumCast(this.layoutDoc._xPadding),
- paddingRight: NumCast(this.layoutDoc._xPadding),
- paddingTop: NumCast(this.layoutDoc._yPadding),
- paddingBottom: NumCast(this.layoutDoc._yPadding),
+ textTransform: StrCast(this.layoutDoc[`${this.fieldKey}_transform`]) as Property.TextTransform,
+ paddingLeft: xmargin,
+ paddingRight: xmargin,
+ paddingTop: ymargin,
+ paddingBottom: ymargin,
width: this._props.PanelWidth(),
height: this._props.PanelHeight(),
whiteSpace: boxParams.multiLine ? 'pre-wrap' : 'pre',
@@ -192,8 +193,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div
key={this._forceRerender}
style={{
- width: this._props.PanelWidth() - 2 * NumCast(this.layoutDoc._xPadding),
- height: this._props.PanelHeight() - 2 * NumCast(this.layoutDoc._yPadding),
+ width: this._props.PanelWidth() - 2 * xmargin,
+ height: this._props.PanelHeight() - 2 * ymargin,
outline: 'unset !important',
}}
onKeyDown={e => {
@@ -214,12 +215,14 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._divRef?.removeEventListener('focusout', this.keepFocus);
this._divRef?.addEventListener('focusout', this.keepFocus);
}}
- onBlur={() => {
+ onBlur={e => {
this._divRef?.removeEventListener('focusout', this.keepFocus);
this.setText(this._divRef?.innerText ?? '');
- RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
- FormattedTextBox.LiveTextUndo?.end();
- FormattedTextBox.LiveTextUndo = undefined;
+ if (!FormattedTextBox.tryKeepingFocus(e.relatedTarget, () => this._divRef?.focus())) {
+ RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
+ FormattedTextBox.LiveTextUndo?.end();
+ FormattedTextBox.LiveTextUndo = undefined;
+ }
}}
dangerouslySetInnerHTML={{
__html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title?.startsWith('#') ? '' : (this.Title ?? '')}</span>`,
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index d5dc256d9..78c8a686c 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -128,6 +128,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
const getAnchor = (field: FieldResult): Element[] => {
const docField = DocCast(field);
const doc = docField?.layout_unrendered ? DocCast(docField.annotationOn, docField) : docField;
+ if (!doc) return [];
const ele = document.getElementById(DocumentView.UniquifyId(DocumentView.LightboxContains(this.DocumentView?.()), doc[Id]));
if (ele?.className === 'linkBox-label') foundParent = true;
if (ele?.getBoundingClientRect().width) return [ele];
@@ -252,7 +253,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 +263,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/DirectionsAnchorMenu.tsx b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
index 8784a709a..8bb7ddd9f 100644
--- a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
+++ b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
@@ -43,7 +43,7 @@ export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
return this._left > 0;
}
- constructor(props: Readonly<{}>) {
+ constructor(props: AntimodeMenuProps) {
super(props);
DirectionsAnchorMenu.Instance = this;
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..55e6d5596 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}
@@ -556,8 +557,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
NativeHeight={this.sidebarNativeHeightFunc}
PanelHeight={this._props.PanelHeight}
PanelWidth={this.sidebarWidth}
- xPadding={0}
- yPadding={0}
+ xMargin={0}
+ yMargin={0}
viewField={this.SidebarKey}
isAnnotationOverlay={false}
originTopLeft
@@ -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..603dcad5c 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"
@@ -337,8 +335,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
select={emptyFunction}
isContentActive={emptyFunction}
NativeDimScaling={returnOne}
- xPadding={25}
- yPadding={10}
+ xMargin={25}
+ yMargin={10}
whenChildContentsActiveChanged={emptyFunction}
removeDocument={returnFalse}
moveDocument={returnFalse}
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 3c4696df3..0f0008700 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -7,7 +7,7 @@ import * as React from 'react';
import axios from 'axios';
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';
@@ -55,7 +55,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();
@@ -454,9 +454,27 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
};
@action
- iframeDown = () => {
- // This is an empty replacement to avoid linter errors
- // The original functionality is no longer needed
+ iframeDown = (e: PointerEvent) => {
+ this._textAnnotationCreator = undefined;
+ const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection();
+ if (sel?.empty && !(e.target as any).textContent)
+ sel.empty(); // Chrome
+ else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
+
+ this._props.select(false);
+ const theclick = this.props
+ .ScreenToLocalTransform()
+ .inverse()
+ .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop));
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+ const target = e.target as HTMLElement;
+ const word = target && getWordAtPoint(target, e.clientX, e.clientY);
+ if (!word && !target?.className?.includes('rangeslider') && !target?.onclick && !target?.parentElement?.onclick) {
+ this.marqueeing = theclick;
+ this._marqueeref.current?.onInitiateSelection(this.marqueeing);
+ this._iframe?.contentDocument?.addEventListener('pointermove', this.iframeMove);
+ e.preventDefault();
+ }
};
isFirefox = () => 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1;
@@ -482,6 +500,122 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
_iframetimeout: NodeJS.Timeout | undefined = undefined;
@observable _warning = 0;
@action
+ iframeLoaded = () => {
+ const iframe = this._iframe;
+ if (this._initialScroll !== undefined) {
+ this.setScrollPos(this._initialScroll);
+ }
+ this._scrollHeight = this._iframe?.contentDocument?.body?.scrollHeight ?? 0;
+ this.addWebStyleSheetRule(this.addWebStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, '');
+
+ let href: Opt<string>;
+ try {
+ href = iframe?.contentWindow?.location.href;
+ } catch {
+ // runInAction(() => this._warning++);
+ href = undefined;
+ }
+ let requrlraw = decodeURIComponent(href?.replace(ClientUtils.prepend('') + '/corsproxy/', '') ?? this._url.toString());
+ if (requrlraw !== this._url.toString()) {
+ if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) {
+ const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g);
+ const newsearch = matches?.lastElement() || '';
+ if (matches) {
+ requrlraw = requrlraw.substring(0, requrlraw.indexOf(newsearch));
+ for (let i = 1; i < Array.from(matches)?.length; i++) {
+ requrlraw = requrlraw.replace(matches[i], '');
+ }
+ }
+ requrlraw = requrlraw
+ .replace(/q=[^&]*/, newsearch.substring(1))
+ .replace('search&', 'search?')
+ .replace('?gbv=1', '');
+ }
+ this.setData(requrlraw);
+ }
+ const iframeContent = iframe?.contentDocument;
+ if (iframeContent) {
+ iframeContent.addEventListener('pointerup', this.iframeUp);
+ iframeContent.addEventListener('pointerdown', this.iframeDown);
+ // iframeContent.addEventListener(
+ // 'wheel',
+ // e => {
+ // e.ctrlKey && e.preventDefault();
+ // },
+ // { passive: false }
+ // );
+ const initHeights = () => {
+ this._scrollHeight = Math.max(this._scrollHeight, iframeContent.body.scrollHeight || 0);
+ if (this._scrollHeight) {
+ this.Document.nativeHeight = Math.min(NumCast(this.Document.nativeHeight), this._scrollHeight);
+ this.layoutDoc.height = Math.min(NumCast(this.layoutDoc._height), (NumCast(this.layoutDoc._width) * NumCast(this.Document.nativeHeight)) / NumCast(this.Document.nativeWidth));
+ }
+ };
+ const swidth = Math.max(NumCast(this.Document.nativeWidth), iframeContent.body.scrollWidth || 0);
+ if (swidth) {
+ const aspectResize = swidth / NumCast(this.Document.nativeWidth, swidth);
+ this.layoutDoc.height = NumCast(this.layoutDoc._height) * aspectResize;
+ this.Document.nativeWidth = swidth;
+ this.Document.nativeHeight = (swidth * NumCast(this.layoutDoc._height)) / NumCast(this.layoutDoc._width);
+ }
+ initHeights();
+ this._iframetimeout && clearTimeout(this._iframetimeout);
+ this._iframetimeout = setTimeout(
+ action(() => initHeights),
+ 5000
+ );
+ iframeContent.addEventListener(
+ 'click',
+ undoable(
+ action((e: MouseEvent) => {
+ let eleHref = (e.target as any)?.outerHTML?.split('"="')[1]?.split('"')[0];
+ for (let ele = e.target as HTMLElement | Element | null; ele; ele = ele.parentElement) {
+ if ('href' in ele) {
+ eleHref = (typeof ele.href === 'string' ? ele.href : eleHref) || (ele.parentElement && 'href' in ele.parentElement ? (ele.parentElement.href as string) : eleHref);
+ }
+ }
+ const origin = this.webField?.origin;
+ if (eleHref && origin) {
+ const batch = UndoManager.StartBatch('webclick');
+ e.stopPropagation();
+ setTimeout(() => {
+ const url = eleHref.replace(ClientUtils.prepend(''), origin);
+ this.setData(url);
+ batch.end();
+ });
+ if (this._outerRef.current) {
+ this._outerRef.current.scrollTop = NumCast(this.layoutDoc._layout_scrollTop);
+ this._outerRef.current.scrollLeft = 0;
+ }
+ }
+ }),
+ 'follow web link'
+ )
+ );
+ iframe.contentDocument.addEventListener('wheel', this.iframeWheel, { passive: false });
+ }
+ };
+
+ @action
+ iframeWheel = (e: WheelEvent) => {
+ if (!this._scrollTimer) {
+ addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' });
+ this._scrollTimer = setTimeout(() => {
+ this._scrollTimer = undefined;
+ clearStyleSheetRules(WebBox.webStyleSheet);
+ }, 250); // this turns events off on the iframe which allows scrolling to change direction smoothly
+ }
+ if (e.ctrlKey) {
+ if (this._innerCollectionView) {
+ this._innerCollectionView.zoom(e.screenX, e.screenY, e.deltaY);
+ const offset = e.clientY - NumCast(this.layoutDoc._layout_scrollTop);
+ this.layoutDoc.freeform_panY = offset - offset / NumCast(this.layoutDoc._freeform_scale) + NumCast(this.layoutDoc._layout_scrollTop) - NumCast(this.layoutDoc._layout_scrollTop) / NumCast(this.layoutDoc._freeform_scale);
+ }
+ e.preventDefault();
+ }
+ };
+
+ @action
setDashScrollTop = (scrollTop: number, timeout: number = 250) => {
const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight());
if (this._scrollTimer) {
@@ -515,8 +649,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) {
@@ -550,13 +684,13 @@ 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;
@@ -568,12 +702,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
if (this._webUrl === this._url) {
this._webUrl = curUrl;
- setTimeout(
- action(() => {
- this._webUrl = this._url;
- this.captureWebScreenshot(); // Capture screenshot for new URL
- })
- );
+ setTimeout(action(() => (this._webUrl = this._url)));
} else {
this._webUrl = this._url;
this.captureWebScreenshot(); // Capture screenshot for new URL
@@ -851,80 +980,36 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// Handle WebField (screenshot of webpage)
if (field instanceof WebField) {
- // Show loading state with spinner
- if (this._isLoadingScreenshot) {
- return (
- <div className="webBox-loading">
- <div className="webBox-loading-message">{this._loadingFromCache ? 'Loading cached webpage preview...' : 'Loading webpage preview...'}</div>
- <div className="webBox-loading-spinner">
- <FontAwesomeIcon className="documentdecorations-icon" icon="spinner" spin />
- </div>
- </div>
- );
- }
-
- // Show error state with retry button
- if (this._screenshotError) {
- return (
- <div className="webBox-error">
- <div className="webBox-error-icon">
- <FontAwesomeIcon icon="exclamation-triangle" size="2x" />
- </div>
- <div className="webBox-error-message">{this._screenshotError}</div>
- <div className="webBox-error-actions">
- <button onClick={() => this.captureWebScreenshot()} className="webBox-retry-button">
- <FontAwesomeIcon icon="sync" style={{ marginRight: '5px' }} />
- Retry
- </button>
- </div>
- </div>
- );
- }
-
- // Show screenshot in scrollable container
- if (this._screenshotUrl) {
- return (
- <div className="webBox-screenshot-container">
- <img
- src={this._screenshotUrl}
- alt="Webpage screenshot"
- className="webBox-screenshot"
- style={{
- width: '100%',
- height: 'auto',
- display: 'block',
- }}
- onError={action((e: React.SyntheticEvent<HTMLImageElement>) => {
- console.error('Error loading screenshot:', e);
- this._screenshotError = 'Failed to load screenshot image';
- this._isLoadingScreenshot = false;
- this.dataDoc[this.fieldKey + '_screenshotUrl'] = undefined;
- this.dataDoc[this.fieldKey + '_screenshotHeight'] = undefined;
- })}
- onLoad={() => {
- this._scrollHeight = this._fullHeight;
- if (this._initialScroll !== undefined) {
- this.setScrollPos(this._initialScroll);
- }
- }}
- />
- </div>
- );
- }
-
- // Fall back to a placeholder if no screenshot yet
+ const url = this.layoutDoc[this.fieldKey + '_useCors'] ? '/corsproxy/' + this._webUrl : this._webUrl;
+ const scripts = this.dataDoc[this.fieldKey + '_allowScripts'] || this._webUrl.includes('wikipedia.org') || this._webUrl.includes('google.com') || this._webUrl.startsWith('https://bing');
+ // if (!scripts) console.log('No scripts for: ' + url);
return (
- <div className="webBox-placeholder">
- <div>Preparing webpage preview...</div>
- </div>
+ <iframe
+ title="web iframe"
+ key={this._warning}
+ className="webBox-iframe"
+ ref={action((r: HTMLIFrameElement | null) => {
+ this._iframe = r;
+ })}
+ style={{ pointerEvents: SnappingManager.IsResizing ? 'none' : undefined }}
+ src={url}
+ onLoad={this.iframeLoaded}
+ scrolling="no" // ugh.. on windows, I get an inner scroll bar for the iframe's body even though the scrollHeight should be set to the full height of the document.
+ // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page
+ // sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />;
+ sandbox={`${scripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`}
+ />
);
}
-
- // Default placeholder
return (
- <div className="webBox-placeholder">
- <div>No content to display</div>
- </div>
+ <iframe
+ title="web frame"
+ className="webBox-iframe"
+ ref={action((r: HTMLIFrameElement | null) => {
+ this._iframe = r;
+ })}
+ src="https://crossorigin.me/https://cs.brown.edu"
+ />
);
}
@@ -1111,18 +1196,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
childPointerEvents = () => (this._props.isContentActive() ? 'all' : undefined);
@computed get webpage() {
TraceMobx();
- const containerWidth = NumCast(this.layoutDoc._width) || this._props.PanelWidth();
+ // const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this._props.pointerEvents?.() as Property.PointerEvents | undefined);
-
+ // const scale = previewScale * (this._props.NativeDimScaling?.() || 1);
return (
<div
className="webBox-outerContent"
ref={this._outerRef}
style={{
- width: '100%',
- height: `${containerWidth}px`,
- overflowY: 'auto',
- overflowX: 'hidden',
+ height: '100%', //`${100 / scale}%`,
pointerEvents,
}}
onWheel={this.onZoomWheel}
@@ -1234,8 +1316,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<div
className="webBox-container"
style={{
- width: `calc(100% - ${this.SidebarShown ? this.sidebarWidth() : 0}px)`,
- height: '100%',
+ width: `calc(${100 / scale}% - ${!this.SidebarShown ? 0 : ((this.sidebarWidth() - WebBox.sidebarResizerWidth) / scale) * (this._previewWidth ? scale : 1)}px)`,
+ height: `${100 / scale}%`,
+ transform: `scale(${scale})`,
pointerEvents,
}}
onContextMenu={this.specificContextMenu}>
@@ -1276,7 +1359,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/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js
index b727107a9..ef465c453 100644
--- a/src/client/views/nodes/WebBoxRenderer.js
+++ b/src/client/views/nodes/WebBoxRenderer.js
@@ -21,7 +21,7 @@ const ForeignHtmlRenderer = function (styleSheets) {
return window.location.origin + extension;
}
function CorsProxy(url) {
- return prepend('/corsProxy/') + encodeURIComponent(url);
+ return prepend('/corsproxy/') + encodeURIComponent(url);
}
/**
*
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx
index 009eb82cd..2b20a666d 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';
@@ -25,7 +24,6 @@ type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDa
export class CalendarBox extends CollectionSubView() {
_calendarRef: HTMLDivElement | null = null;
_calendar: Calendar | undefined;
- _oldWheel: HTMLElement | null = null;
_observer: ResizeObserver | undefined;
_eventsDisposer: IReactionDisposer | undefined;
_selectDisposer: IReactionDisposer | undefined;
@@ -106,14 +104,15 @@ export class CalendarBox extends CollectionSubView() {
// TODO: Return a different color based on the event type
eventToColor = (event: Doc): string => {
- return 'red';
+ return 'red' + event;
};
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
internalDocDrop = (e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) => {
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;
};
@@ -194,7 +193,6 @@ export class CalendarBox extends CollectionSubView() {
setTimeout(() => cal?.view.calendar.select(this.dateSelect.start, this.dateSelect.end));
};
- onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
render() {
return (
<div
@@ -218,10 +216,7 @@ export class CalendarBox extends CollectionSubView() {
}}
ref={r => {
this.createDashEventsTarget(r);
- this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
- this._oldWheel = r;
- // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
- r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+ this.fixWheelEvents(r, this._props.isContentActive);
if (r) {
this._observer?.disconnect();
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index d45c6c936..d919b5f7f 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -263,7 +263,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// Add CSV details to linked files
this._linked_csv_files.push({
- filename: CsvCast(newLinkedDoc.data).url.pathname,
+ filename: CsvCast(newLinkedDoc.data)?.url.pathname ?? '',
id: csvId,
text: csvData,
});
@@ -832,7 +832,7 @@ 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);
if (existingDoc) {
diff --git a/src/client/views/nodes/chatbot/tools/GetDocsTool.ts b/src/client/views/nodes/chatbot/tools/GetDocsTool.ts
index 05482a66e..42a7747d3 100644
--- a/src/client/views/nodes/chatbot/tools/GetDocsTool.ts
+++ b/src/client/views/nodes/chatbot/tools/GetDocsTool.ts
@@ -40,7 +40,10 @@ export class GetDocsTool extends BaseTool<GetDocsToolParamsType> {
}
async execute(args: ParametersType<GetDocsToolParamsType>): Promise<Observation[]> {
- const docs = args.document_ids.map(doc_id => DocCast(DocServer.GetCachedRefField(doc_id)));
+ const docs = args.document_ids
+ .map(doc_id => DocCast(DocServer.GetCachedRefField(doc_id)))
+ .filter(d => d)
+ .map(d => d!);
const collection = Docs.Create.FreeformDocument(docs, { title: args.title });
this._docView._props.addDocTab(collection, OpenWhere.addRight);
return [{ type: 'text', text: `Collection created in Dash called ${args.title}` }];
diff --git a/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts b/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts
index 4268c0180..3df1294e9 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, docManager: AgentDocumentManager) {
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.
@@ -100,7 +101,7 @@ export class Vectorstore {
} else {
// Start processing the document.
doc.ai_document_status = 'PROGRESS';
- const local_file_path: string = CsvCast(doc.data)?.url?.pathname ?? PDFCast(doc.data)?.url?.pathname ?? VideoCast(doc.data)?.url?.pathname ?? AudioCast(doc.data)?.url?.pathname;
+ const local_file_path = CsvCast(doc.data)?.url?.pathname ?? PDFCast(doc.data)?.url?.pathname ?? VideoCast(doc.data)?.url?.pathname ?? AudioCast(doc.data)?.url?.pathname;
if (!local_file_path) {
console.log('Not adding to vectorstore. Invalid file path for vectorstore addition.');
@@ -316,7 +317,7 @@ export class Vectorstore {
encoding_format: 'float',
});
- let queryEmbedding = queryEmbeddingResponse.data[0].embedding;
+ const queryEmbedding = queryEmbeddingResponse.data[0].embedding;
// Get document IDs from the AgentDocumentManager
const docIds = Array.from(this.docManager.listDocs());
diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx
index ec1f7a023..871c556e6 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)) {
+ const 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 2e2e1d41c..3734ad9cc 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.scss
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -26,6 +26,7 @@
display: inline-block;
font-weight: normal;
background: rgba(0, 0, 0, 0.1);
+ align-content: center;
cursor: default;
}
.dashFieldView-fieldSpan {
@@ -42,6 +43,9 @@
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 aa2829aaf..7ea5d1fcf 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -162,7 +162,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
this._expanded = !this._props.editable ? false : !this._expanded;
})}>
<SchemaTableCell
- Document={this._dashDoc}
+ Doc={this._dashDoc}
col={0}
deselectCell={emptyFunction}
selectCell={() => (this._expanded ? true : undefined)}
@@ -184,7 +184,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
getFinfo={this.finfo}
setColumnValues={returnFalse}
allowCRs
- oneLine={!this._expanded}
+ oneLine={!this._expanded && this._props.editable}
finishEdit={this.finishEdit}
transform={Transform.Identity}
menuTarget={null}
@@ -275,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 => (
@@ -308,6 +308,7 @@ export class DashFieldView {
this.dom.style.height = node.attrs.height;
this.dom.style.position = 'relative';
this.dom.style.display = 'inline-flex';
+ this.dom.style.maxWidth = '100%';
this.dom.onkeypress = function (e: KeyboardEvent) {
e.stopPropagation();
};
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 7b24125e7..57720baae 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,14 +13,13 @@ 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, 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';
@@ -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);
@@ -245,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,
@@ -309,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 + ':') + //
@@ -317,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 '';
@@ -429,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
@@ -442,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));
@@ -451,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();
}
@@ -588,8 +586,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return true;
}
const dragData = de.complete.docDragData;
- if (dragData) {
- const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
+ if (dragData && !this._props.rejectDrop?.(de, this.DocumentView?.())) {
+ 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>;
@@ -597,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]);
}
@@ -697,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)
};
@@ -743,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');
@@ -769,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;
@@ -847,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...');
@@ -944,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...');
@@ -980,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')),
@@ -1034,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();
}
}
@@ -1067,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)));
@@ -1212,18 +1147,28 @@ 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 && NumCast(this.layoutDoc._nativeHeight)) 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() {
- return this.DocumentView?.().showTags ? Math.max(0, 20 - Math.max(this._props.yPadding ?? 0, NumCast(this.layoutDoc._yMargin))) * this.ScreenToLocalBoxXf().Scale : 0;
+ return this.DocumentView?.().showTags ? Math.max(0, 20 - Math.max(this._props.yMargin ?? 0, NumCast(this.layoutDoc._yMargin))) * this.ScreenToLocalBoxXf().Scale : 0;
}
@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.
@@ -1233,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),
@@ -1253,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)) &&
@@ -1262,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
@@ -1277,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)) };
@@ -1286,13 +1224,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
incomingValue => {
if (this.EditorView && this.ApplyingChange !== this.fieldKey) {
if (incomingValue?.data) {
- const updatedState = JSON.parse(incomingValue.data.Data);
+ const updatedState = JSON.parse(incomingValue.data.Data.replace(/\n/g, ''));
if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) {
this.EditorView.updateState(EditorState.fromJSON(this.config, updatedState));
this.tryUpdateScrollHeight();
}
} else if (this.EditorView.state.doc.textContent !== (incomingValue?.str ?? '')) {
selectAll(this.EditorView.state, tx => this.EditorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
+ this.tryUpdateScrollHeight();
}
}
},
@@ -1317,7 +1256,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) {
@@ -1333,14 +1272,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),
@@ -1374,7 +1313,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));
@@ -1397,7 +1336,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';
}
},
@@ -1406,8 +1345,62 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return text;
};
- handlePaste = (view: EditorView, event: Event /* , slice: Slice */): boolean => {
- const pdfAnchorId = (event as ClipboardEvent).clipboardData?.getData('dash/pdfAnchor');
+ handlePaste = (view: EditorView, event: ClipboardEvent /* , slice: Slice */): boolean => {
+ return this.doPaste(view, event.clipboardData);
+ };
+ doPaste = (view: EditorView, data: DataTransfer | null) => {
+ const html = data?.getData('text/html');
+ const pdfAnchorId = data?.getData('dash/pdfAnchor');
+ if (html && !pdfAnchorId) {
+ const replaceDivsWithParagraphs = (expr: string) => {
+ // Create a temporary DOM container
+ const container = document.createElement('div');
+ container.innerHTML = expr;
+
+ // Recursive function to process all divs
+ function processDivs(node: HTMLElement) {
+ // Get all div elements in the current node (live collection)
+ const divs = node.getElementsByTagName('div');
+
+ // We need to convert to array because we'll be modifying the DOM
+ const divsArray = Array.from(divs);
+
+ for (const div of divsArray) {
+ // Create replacement paragraph
+ const p = document.createElement('p');
+
+ // Copy all attributes
+ for (const attr of div.attributes) {
+ p.setAttribute(attr.name, attr.value);
+ }
+
+ // Move all child nodes
+ while (div.firstChild) {
+ p.appendChild(div.firstChild);
+ }
+
+ // Replace the div with the paragraph
+ div.parentNode?.replaceChild(p, div);
+
+ // Process any nested divs that were moved into the new paragraph
+ processDivs(p);
+ }
+ }
+
+ // Start processing from the container
+ processDivs(container);
+
+ return container.innerHTML;
+ };
+ const fixedHTML = replaceDivsWithParagraphs(html);
+ // .replace(/<div\b([^>]*)>(.*?)<\/div>/g, '<p$1>$2</p>'); // prettier-ignore
+ this._inDrop = true;
+ view.pasteHTML(html.split('<p').length < 2 ? fixedHTML : html);
+ this._inDrop = false;
+
+ return true;
+ }
+
return !!(pdfAnchorId && this.addPdfReference(pdfAnchorId));
};
@@ -1458,42 +1451,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') {
@@ -1509,34 +1517,31 @@ 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();
}
if (this._props.isContentActive()) this.prepareForTyping();
if (this.EditorView && FormattedTextBox.PasteOnLoad) {
- const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor');
+ this.doPaste(this.EditorView, FormattedTextBox.PasteOnLoad.clipboardData);
FormattedTextBox.PasteOnLoad = undefined;
- pdfAnchorId && this.addPdfReference(pdfAnchorId);
}
if (this._props.autoFocus) setTimeout(() => this.EditorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it.
}
@@ -1554,9 +1559,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();
@@ -1590,7 +1596,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;
@@ -1745,19 +1751,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
* When a text box loses focus, it might be because a text button was clicked (eg, bold, italics) or color picker.
* In these cases, force focus back onto the text box.
* @param target
+ * @returns true if focus was kept on the text box, false otherwise
*/
- tryKeepingFocus = (target: Element | null) => {
+ public static tryKeepingFocus(target: Element | null, refocusFunc?: () => void) {
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') {
- return this.EditorView?.focus(); // keep focus on text box
+ // 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 ?? '')) {
+ refocusFunc?.(); // keep focus on text box
+ return true;
}
}
- };
+ return false;
+ }
@action
onBlur = (e: React.FocusEvent) => {
- this.tryKeepingFocus(e.relatedTarget);
+ FormattedTextBox.tryKeepingFocus(e.relatedTarget, () => this.EditorView?.focus());
if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) {
const stordMarks = this.EditorView?.state.storedMarks?.slice();
@@ -1803,13 +1812,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);
@@ -1858,7 +1884,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
};
tryUpdateScrollHeight = () => {
- const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
+ const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yMargin || 0);
const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
if (this.EditorView && children && !SnappingManager.IsDragging) {
const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
@@ -1873,10 +1899,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...
@@ -1885,7 +1911,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);
@@ -1893,17 +1919,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 =>
@@ -1913,7 +1939,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
returnFalse,
emptyFunction,
action(() => {
- this._recordingDictation = !this._recordingDictation;
+ this.recordingDictation = !this.recordingDictation;
})
)
}>
@@ -1921,6 +1947,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;
@@ -1935,6 +1975,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
backgroundColor,
color,
opacity: annotated ? 1 : undefined,
+ transform: `scale(${this.uiBtnScaling})`,
}}>
<FontAwesomeIcon icon="comment-alt" />
</div>
@@ -1948,7 +1989,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
@@ -1979,7 +2020,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}
@@ -1996,7 +2037,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>
);
@@ -2044,7 +2085,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) => {
@@ -2054,7 +2095,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
@@ -2071,26 +2112,19 @@ 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()??1), view?._props.PanelHeight() / (view?.screenToLocalScale()??1)]; // 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.xMargin ?? 0, this._props.screenXPadding?.(this._props.DocumentView?.()) ?? 0);
+ const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, this._props.yMargin ?? 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} />
) : styleFromLayout?.height === '0px' ? null : (
<div
className="formattedTextBox"
- ref={r => {
- this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
- this._oldWheel = r;
- r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
- }}
+ ref={r => this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel)}
style={{
...(this._props.dontScale
? {}
@@ -2100,7 +2134,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,
@@ -2133,13 +2166,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),
@@ -2153,7 +2186,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 f3ec6cc9d..f26a75fe4 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) {
@@ -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;
}
@@ -361,7 +349,8 @@ export class RichTextRules {
let count = 0; // ignore first return value which will be the notation that chat is pending a result
Doc.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => {
if (count) {
- const tr = this.TextBox.EditorView?.state.tr.insertText(' ' + (gptval as string));
+ this.TextBox.EditorView?.pasteText(' ' + (gptval as string), undefined);
+ const tr = this.TextBox.EditorView?.state.tr; //.insertText(' ' + (gptval as string));
tr && this.TextBox.EditorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(end + 2), tr.doc.resolve(end + 2 + (gptval as string).length))));
RichTextMenu.Instance?.elideSelection(this.TextBox.EditorView?.state, true);
}
@@ -399,11 +388,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 +405,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..198b8e713 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;
@@ -281,11 +281,14 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
try {
const canvasOriginalImg = ImageUtility.getCanvasImg(img);
if (!canvasOriginalImg) return;
- const canvasMask = ImageUtility.getCanvasMask(canvas, canvasOriginalImg);
+ const canvasMask = ImageUtility.getCanvasMask(canvas, canvas);
if (!canvasMask) return;
const maskBlob = await ImageUtility.canvasToBlob(canvasMask);
const imgBlob = await ImageUtility.canvasToBlob(canvasOriginalImg);
const res = await ImageUtility.getEdit(imgBlob, maskBlob, input || 'Fill in the image in the same style', 2);
+ if ((res as any).status == 'error') {
+ alert((res as any).message);
+ }
// create first image
if (!newCollectionRef.current) {
@@ -397,9 +400,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 +478,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/imageEditorUtils/ImageHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts
index 1c6a38a24..d6093c6eb 100644
--- a/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts
+++ b/src/client/views/nodes/imageEditor/imageEditorUtils/ImageHandler.ts
@@ -75,7 +75,7 @@ export class ImageUtility {
fd.append('mask', maskBlob, 'mask.png');
fd.append('prompt', prompt);
fd.append('size', '1024x1024');
- fd.append('n', n ? JSON.stringify(n) : '1');
+ fd.append('n', n ? n + '' : '1');
fd.append('response_format', 'b64_json');
try {
@@ -268,14 +268,14 @@ export class ImageUtility {
ctx.drawImage(img, xOffset, 0, width, height);
// draw reflected image padding
- this.drawHorizontalReflection(ctx, canvas, xOffset);
+ // this.drawHorizontalReflection(ctx, canvas, xOffset);
} else {
// vertical padding, y offset
const yOffset = Math.floor((canvasSize - height) / 2);
ctx.drawImage(img, 0, yOffset, width, height);
// draw reflected image padding
- this.drawVerticalReflection(ctx, canvas, yOffset);
+ // this.drawVerticalReflection(ctx, canvas, yOffset);
}
return canvas;
};
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..e580c7070
--- /dev/null
+++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx
@@ -0,0 +1,81 @@
+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, 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/scrapbook/EmbeddedDocView.tsx b/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx
new file mode 100644
index 000000000..e99bf67c7
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx
@@ -0,0 +1,52 @@
+//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
+import * as React from "react";
+import { observer } from "mobx-react";
+import { Doc } from "../../../../fields/Doc";
+import { DocumentView } from "../DocumentView";
+import { Transform } from "../../../util/Transform";
+
+interface EmbeddedDocViewProps {
+ doc: Doc;
+ width?: number;
+ height?: number;
+ slotId?: string;
+}
+
+@observer
+export class EmbeddedDocView extends React.Component<EmbeddedDocViewProps> {
+ render() {
+ const { doc, width = 300, height = 200, slotId } = this.props;
+
+ // Use either an existing embedding or create one
+ let docToDisplay = doc;
+
+ // If we need an embedding, create or use one
+ if (!docToDisplay.isEmbedding) {
+ docToDisplay = Doc.BestEmbedding(doc) || Doc.MakeEmbedding(doc);
+ // Set the container to the slot's ID so we can track it
+ if (slotId) {
+ docToDisplay.embedContainer = `scrapbook-slot-${slotId}`;
+ }
+ }
+
+ return (
+ <DocumentView
+ Document={docToDisplay}
+ renderDepth={0}
+ // Required sizing functions
+ NativeWidth={() => width}
+ NativeHeight={() => height}
+ PanelWidth={() => width}
+ PanelHeight={() => height}
+ // Required state functions
+ isContentActive={() => true}
+ childFilters={() => []}
+ ScreenToLocalTransform={() => new Transform()}
+ // Display options
+ hideDeleteButton={true}
+ hideDecorations={true}
+ hideResizeHandles={true}
+ />
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
new file mode 100644
index 000000000..6cfe9a62c
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -0,0 +1,143 @@
+import { action, makeObservable, observable } from 'mobx';
+import * as React from 'react';
+import { Doc, DocListCast } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
+import { emptyFunction } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { CollectionView } from '../../collections/CollectionView';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { DocumentView } from '../DocumentView';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { DragManager } from '../../../util/DragManager';
+import { RTFCast, StrCast, toList } from '../../../../fields/Types';
+import { undoable } from '../../../util/UndoManager';
+// Scrapbook view: a container that lays out its child items in a grid/template
+export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ @observable createdDate: string;
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ makeObservable(this);
+ this.createdDate = this.getFormattedDate();
+
+ // ensure we always have a List<Doc> in dataDoc['items']
+ if (!this.dataDoc[this.fieldKey]) {
+ this.dataDoc[this.fieldKey] = new List<Doc>();
+ }
+ this.createdDate = this.getFormattedDate();
+ this.setTitle();
+ }
+
+ public static LayoutString(fieldStr: string) {
+ return FieldView.LayoutString(ScrapbookBox, fieldStr);
+ }
+
+ getFormattedDate(): string {
+ return new Date().toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ }
+
+ @action
+ setTitle() {
+ const title = `Scrapbook - ${this.createdDate}`;
+ if (this.dataDoc.title !== title) {
+ this.dataDoc.title = title;
+
+ const image = Docs.Create.TextDocument('image');
+ image.accepts_docType = DocumentType.IMG;
+ const placeholder = new Doc();
+ placeholder.proto = image;
+ placeholder.original = image;
+ placeholder._width = 250;
+ placeholder._height = 200;
+ placeholder.x = 0;
+ placeholder.y = -100;
+ //placeholder.overrideFields = new List<string>(['x', 'y']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos
+
+ const summary = Docs.Create.TextDocument('summary');
+ summary.accepts_docType = DocumentType.RTF;
+ summary.accepts_textType = 'one line';
+ const placeholder2 = new Doc();
+ placeholder2.proto = summary;
+ placeholder2.original = summary;
+ placeholder2.x = 0;
+ placeholder2.y = 200;
+ placeholder2._width = 250;
+ //placeholder2.overrideFields = new List<string>(['x', 'y', '_width']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos
+ this.dataDoc[this.fieldKey] = new List<Doc>([placeholder, placeholder2]);
+ }
+ }
+
+ componentDidMount() {
+ this.setTitle();
+ }
+
+ childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => {
+ return true; // disable dropping documents onto any child of the scrapbook.
+ };
+ rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => {
+ // Test to see if the dropped doc is dropped on an acceptable location (anywerhe? on a specific box).
+ // const draggedDocs = de.complete.docDragData?.draggedDocuments;
+ return false; // allow all Docs to be dropped onto scrapbook -- let filterAddDocument make the final decision.
+ };
+
+ filterAddDocument = (docIn: Doc | Doc[]) => {
+ const docs = toList(docIn);
+ if (docs?.length === 1) {
+ const placeholder = DocListCast(this.dataDoc[this.fieldKey]).find(d =>
+ (d.accepts_docType === docs[0].$type || // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type
+ RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type)))
+ ); // prettier-ignore
+
+ if (placeholder) {
+ // ugh. we have to tell the underlying view not to add the Doc so that we can add it where we want it.
+ // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo.
+ setTimeout(
+ undoable(() => {
+ //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field])); // // shouldn't need to do this for layout fields since the placeholder already overrides its protos
+ placeholder.proto = docs[0];
+ }, 'Scrapbook add')
+ );
+ return false;
+ }
+ }
+ return false;
+ };
+
+ render() {
+ return (
+ <div style={{ background: 'beige', width: '100%', height: '100%' }}>
+ <CollectionView
+ {...this._props} //
+ setContentViewBox={emptyFunction}
+ rejectDrop={this.rejectDrop}
+ childRejectDrop={this.childRejectDrop}
+ filterAddDocument={this.filterAddDocument}
+ />
+ {/* <div style={{ border: '1px black', borderStyle: 'dotted', position: 'absolute', top: '50%', width: '100%', textAlign: 'center' }}>Drop an image here</div> */}
+ </div>
+ );
+ }
+}
+
+// Register scrapbook
+Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, {
+ layout: { view: ScrapbookBox, dataField: 'items' },
+ options: {
+ acl: '',
+ _height: 200,
+ _xMargin: 10,
+ _yMargin: 10,
+ _layout_fitWidth: false,
+ _layout_autoHeight: true,
+ _layout_reflowVertical: true,
+ _layout_reflowHorizontal: true,
+ _freeform_fitContentsToBox: true,
+ defaultDoubleClick: 'ignore',
+ systemIcon: 'BsImages',
+ },
+});
diff --git a/src/client/views/nodes/scrapbook/ScrapbookContent.tsx b/src/client/views/nodes/scrapbook/ScrapbookContent.tsx
new file mode 100644
index 000000000..ad1d308e8
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookContent.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import { observer } from "mobx-react-lite";
+// Import the Doc type from your actual module.
+import { Doc } from "../../../../fields/Doc";
+
+export interface ScrapbookContentProps {
+ doc: Doc;
+}
+
+// A simple view that displays a document's title and content.
+// Adjust how you extract the text if your Doc fields are objects.
+export const ScrapbookContent: React.FC<ScrapbookContentProps> = observer(({ doc }) => {
+ // If doc.title or doc.content are not plain strings, convert them.
+ const titleText = doc.title ? doc.title.toString() : "Untitled";
+ const contentText = doc.content ? doc.content.toString() : "No content available.";
+
+ return (
+ <div className="scrapbook-content">
+ <h3>{titleText}</h3>
+ <p>{contentText}</p>
+ </div>
+ );
+});
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss
new file mode 100644
index 000000000..ae647ad36
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss
@@ -0,0 +1,85 @@
+//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
+.scrapbook-slot {
+ position: absolute;
+ background-color: rgba(245, 245, 245, 0.7);
+ border: 2px dashed #ccc;
+ border-radius: 5px;
+ box-sizing: border-box;
+ transition: all 0.2s ease;
+ overflow: hidden;
+
+ &.scrapbook-slot-over {
+ border-color: #4a90e2;
+ background-color: rgba(74, 144, 226, 0.1);
+ }
+
+ &.scrapbook-slot-filled {
+ border-style: solid;
+ border-color: rgba(0, 0, 0, 0.1);
+ background-color: transparent;
+
+ &.scrapbook-slot-over {
+ border-color: #4a90e2;
+ background-color: rgba(74, 144, 226, 0.1);
+ }
+ }
+
+ .scrapbook-slot-empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ }
+
+ .scrapbook-slot-placeholder {
+ text-align: center;
+ color: #888;
+ }
+
+ .scrapbook-slot-title {
+ font-weight: bold;
+ margin-bottom: 5px;
+ }
+
+ .scrapbook-slot-instruction {
+ font-size: 0.9em;
+ font-style: italic;
+ }
+
+ .scrapbook-slot-content {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ }
+
+ .scrapbook-slot-controls {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ z-index: 10;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+
+ .scrapbook-slot-remove-btn {
+ background-color: rgba(255, 255, 255, 0.8);
+ border: 1px solid #ccc;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 10px;
+
+ &:hover {
+ background-color: rgba(255, 0, 0, 0.1);
+ }
+ }
+ }
+
+ &:hover .scrapbook-slot-controls {
+ opacity: 1;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx
new file mode 100644
index 000000000..2c8f93778
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx
@@ -0,0 +1,28 @@
+
+//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
+export interface SlotDefinition {
+ id: string;
+ x: number; y: number;
+ defaultWidth: number;
+ defaultHeight: number;
+ }
+
+ export interface SlotContentMap {
+ slotId: string;
+ docId?: string;
+ }
+
+ export interface ScrapbookConfig {
+ slots: SlotDefinition[];
+ contents?: SlotContentMap[];
+ }
+
+ export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = {
+ slots: [
+ { id: "slot1", x: 10, y: 10, defaultWidth: 180, defaultHeight: 120 },
+ { id: "slot2", x: 200, y: 10, defaultWidth: 180, defaultHeight: 120 },
+ // …etc
+ ],
+ contents: []
+ };
+ \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts
new file mode 100644
index 000000000..686917d9a
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts
@@ -0,0 +1,25 @@
+// ScrapbookSlotTypes.ts
+export interface SlotDefinition {
+ id: string;
+ title: string;
+ x: number;
+ y: number;
+ defaultWidth: number;
+ defaultHeight: number;
+ }
+
+ export interface ScrapbookConfig {
+ slots: SlotDefinition[];
+ contents?: { slotId: string; docId: string }[];
+ }
+
+ // give it three slots by default:
+ export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = {
+ slots: [
+ { id: "main", title: "Main Content", x: 20, y: 20, defaultWidth: 360, defaultHeight: 200 },
+ { id: "notes", title: "Notes", x: 20, y: 240, defaultWidth: 360, defaultHeight: 160 },
+ { id: "resources", title: "Resources", x: 400, y: 20, defaultWidth: 320, defaultHeight: 380 },
+ ],
+ contents: [],
+ };
+ \ No newline at end of file
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index f818c6e20..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() {
@@ -273,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?.();
}
@@ -295,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))
@@ -325,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));
});
@@ -346,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;
@@ -364,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) {
@@ -395,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];
@@ -422,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;
}
@@ -452,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();
@@ -516,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) {
@@ -528,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(() => {
@@ -554,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;
@@ -596,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) {
@@ -656,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;
@@ -669,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 })
@@ -690,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);
}
});
@@ -741,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);
@@ -757,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) {
@@ -807,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);
});
};
@@ -941,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) {
@@ -970,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();
@@ -1061,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))
@@ -1078,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>
);
@@ -1278,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)}>
@@ -1527,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() {
@@ -1674,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
@@ -1798,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] }} />
@@ -1948,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}
/>
@@ -2146,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()}>
@@ -2700,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
@@ -2893,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
@@ -2985,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 31cd1603f..55a655c7a 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();
@@ -56,7 +56,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
.containerViewPath?.()
.slice()
.reverse()
- .find(dv => dv?.ComponentView instanceof PresBox)?.ComponentView as PresBox;
+ .find(dv => dv?.ComponentView instanceof PresBox)?.ComponentView as Opt<PresBox>;
}
// the presentation view document that renders this slide
@@ -67,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
@@ -118,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}
@@ -349,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);
};
@@ -364,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'
);
@@ -374,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
@@ -383,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
@@ -413,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
@@ -452,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>
@@ -559,10 +559,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
style={{
backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent',
opacity: this._dragging ? 0.3 : 1,
- paddingLeft: NumCast(this.layoutDoc._xPadding, this._props.xPadding),
- paddingRight: NumCast(this.layoutDoc._xPadding, this._props.xPadding),
- paddingTop: NumCast(this.layoutDoc._yPadding, this._props.yPadding),
- paddingBottom: NumCast(this.layoutDoc._yPadding, this._props.yPadding),
+ paddingLeft: NumCast(this.layoutDoc._xMargin, this._props.xMargin),
+ paddingRight: NumCast(this.layoutDoc._xMargin, this._props.xMargin),
+ paddingTop: NumCast(this.layoutDoc._yPadding, this._props.yMargin),
+ paddingBottom: NumCast(this.layoutDoc._yPadding, this._props.yMargin),
}}
onDoubleClick={action(() => {
this.toggleProperties();
@@ -622,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';