aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoreleanor-park <eleanor_park@brown.edu>2024-10-30 19:39:46 -0400
committereleanor-park <eleanor_park@brown.edu>2024-10-30 19:39:46 -0400
commitc11c760db62f78a07b624b98b209e6ee86036c8e (patch)
treec9587b50042a5115373e91ba8ecf9b76913cd321
parentb5944e87f9d4f3149161de4de0d76db486461c76 (diff)
parent4c768162e0436115a05b9c8b0e4d837d626d45ba (diff)
Merge branch 'master' into eleanor-gptdraw
-rw-r--r--package-lock.json8
-rw-r--r--package.json2
-rw-r--r--src/ClientUtils.ts11
-rw-r--r--src/client/apis/gpt/GPT.ts15
-rw-r--r--src/client/documents/DocUtils.ts25
-rw-r--r--src/client/documents/DocumentTypes.ts24
-rw-r--r--src/client/documents/Documents.ts74
-rw-r--r--src/client/util/CurrentUserUtils.ts102
-rw-r--r--src/client/util/DocumentManager.ts8
-rw-r--r--src/client/util/InteractionUtils.tsx6
-rw-r--r--src/client/util/LinkFollower.ts2
-rw-r--r--src/client/util/Scripting.ts12
-rw-r--r--src/client/util/SettingsManager.tsx28
-rw-r--r--src/client/util/SnappingManager.ts7
-rw-r--r--src/client/util/node_modules/type_decls.d3
-rw-r--r--src/client/views/DocumentDecorations.tsx4
-rw-r--r--src/client/views/FilterPanel.tsx2
-rw-r--r--src/client/views/GestureOverlay.tsx171
-rw-r--r--src/client/views/GlobalKeyHandler.ts9
-rw-r--r--src/client/views/InkingStroke.tsx3
-rw-r--r--src/client/views/LightboxView.tsx4
-rw-r--r--src/client/views/MarqueeAnnotator.tsx3
-rw-r--r--src/client/views/PinFuncs.ts23
-rw-r--r--src/client/views/SidebarAnnos.tsx4
-rw-r--r--src/client/views/StyleProp.ts4
-rw-r--r--src/client/views/StyleProvider.tsx12
-rw-r--r--src/client/views/StyleProviderQuiz.tsx2
-rw-r--r--src/client/views/TagsView.tsx2
-rw-r--r--src/client/views/ViewBoxInterface.ts2
-rw-r--r--src/client/views/collections/CollectionCardDeckView.scss24
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx390
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx82
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx94
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx2
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx5
-rw-r--r--src/client/views/collections/CollectionSubView.tsx19
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx4
-rw-r--r--src/client/views/collections/CollectionView.tsx6
-rw-r--r--src/client/views/collections/FlashcardPracticeUI.scss6
-rw-r--r--src/client/views/collections/FlashcardPracticeUI.tsx59
-rw-r--r--src/client/views/collections/TabDocView.tsx1
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx94
-rw-r--r--src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx23
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/SchemaRowBox.tsx2
-rw-r--r--src/client/views/global/globalScripts.ts160
-rw-r--r--src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx6
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx5
-rw-r--r--src/client/views/nodes/ComparisonBox.scss6
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx886
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx6
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx38
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx1
-rw-r--r--src/client/views/nodes/DocumentView.tsx114
-rw-r--r--src/client/views/nodes/EquationBox.tsx2
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.scss7
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx190
-rw-r--r--src/client/views/nodes/ImageBox.tsx8
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx2
-rw-r--r--src/client/views/nodes/LabelBox.scss1
-rw-r--r--src/client/views/nodes/LabelBox.tsx168
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx2
-rw-r--r--src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx5
-rw-r--r--src/client/views/nodes/PDFBox.tsx2
-rw-r--r--src/client/views/nodes/VideoBox.tsx2
-rw-r--r--src/client/views/nodes/WebBox.tsx13
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/Agent.ts82
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx3
-rw-r--r--src/client/views/nodes/chatbot/tools/BaseTool.ts82
-rw-r--r--src/client/views/nodes/chatbot/tools/CalculateTool.ts30
-rw-r--r--src/client/views/nodes/chatbot/tools/CreateCSVTool.ts45
-rw-r--r--src/client/views/nodes/chatbot/tools/CreateCollectionTool.ts36
-rw-r--r--src/client/views/nodes/chatbot/tools/DataAnalysisTool.ts37
-rw-r--r--src/client/views/nodes/chatbot/tools/GetDocsTool.ts38
-rw-r--r--src/client/views/nodes/chatbot/tools/NoTool.ts23
-rw-r--r--src/client/views/nodes/chatbot/tools/RAGTool.ts33
-rw-r--r--src/client/views/nodes/chatbot/tools/SearchTool.ts65
-rw-r--r--src/client/views/nodes/chatbot/tools/ToolTypes.ts76
-rw-r--r--src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts114
-rw-r--r--src/client/views/nodes/chatbot/tools/WikipediaTool.ts33
-rw-r--r--src/client/views/nodes/chatbot/types/types.ts16
-rw-r--r--src/client/views/nodes/formattedText/DashDocCommentView.tsx4
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx288
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts4
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx92
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts2
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx57
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx34
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx23
-rw-r--r--src/client/views/pdf/PDFViewer.tsx2
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.tsx12
-rw-r--r--src/extensions/ExtensionsTypings.ts8
-rw-r--r--src/extensions/Extensions_Array.ts10
-rw-r--r--src/fields/Doc.ts21
-rw-r--r--src/fields/InkField.ts27
-rw-r--r--src/fields/RichTextField.ts23
-rw-r--r--src/fields/ScriptField.ts6
-rw-r--r--src/fields/documentSchemas.ts7
-rw-r--r--src/fields/util.ts1
-rw-r--r--src/pen-gestures/GestureTypes.ts1
-rw-r--r--src/pen-gestures/ndollar.ts96
-rw-r--r--src/server/ApiManagers/AssistantManager.ts116
-rw-r--r--src/server/GarbageCollector.ts3
-rw-r--r--src/server/server_Initialization.ts2
106 files changed, 2542 insertions, 2031 deletions
diff --git a/package-lock.json b/package-lock.json
index 4e95fcee0..43dcb1c3f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -72,7 +72,7 @@
"body-parser": "^1.20.2",
"bootstrap": "^5.3.2",
"brotli": "^1.3.3",
- "browndash-components": "^0.1.50",
+ "browndash-components": "0.1.53",
"browser-assert": "^1.2.1",
"bson": "^6.2.0",
"canvas": "^2.11.2",
@@ -13575,9 +13575,9 @@
}
},
"node_modules/browndash-components": {
- "version": "0.1.50",
- "resolved": "https://registry.npmjs.org/browndash-components/-/browndash-components-0.1.50.tgz",
- "integrity": "sha512-8EgU82os8/Tg3gayh+nYXCQZ8P7GGQWnibz0U2RM0sXTbc3BWTSYiC1Sj1VpL7HPkZ1KMGHzWynCo9QKIOWZBg==",
+ "version": "0.1.53",
+ "resolved": "https://registry.npmjs.org/browndash-components/-/browndash-components-0.1.53.tgz",
+ "integrity": "sha512-lBZlXe2u/6d9NlrR+fH+9ncCFGRUxGqJRejpfhbhVmQ4Q4uvP9LlmTuaVQElqys5wGUyK0SuqrLYPGhJthdQLQ==",
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
diff --git a/package.json b/package.json
index 06179fd7d..63d5e387d 100644
--- a/package.json
+++ b/package.json
@@ -155,7 +155,7 @@
"body-parser": "^1.20.2",
"bootstrap": "^5.3.2",
"brotli": "^1.3.3",
- "browndash-components": "^0.1.50",
+ "browndash-components": "0.1.53",
"browser-assert": "^1.2.1",
"bson": "^6.2.0",
"canvas": "^2.11.2",
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index e7aee1c2a..baad9a06c 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -107,12 +107,12 @@ export namespace ClientUtils {
default: return type.charAt(0).toUpperCase() + type.substring(1,3);
} // prettier-ignore
}
- export function cleanDocumentType(type: DocumentType, colType: CollectionViewType) {
+ export function cleanDocumentType(type: DocumentType, colType?: CollectionViewType) {
switch (type) {
case DocumentType.PDF: return 'PDF';
case DocumentType.IMG: return 'Image';
case DocumentType.AUDIO: return 'Audio';
- case DocumentType.COL: return 'Collection:'+colType;
+ case DocumentType.COL: return 'Collection:'+ (colType ?? "");
case DocumentType.RTF: return 'Text';
default: return type.charAt(0).toUpperCase() + type.slice(1);
} // prettier-ignore
@@ -212,7 +212,7 @@ export namespace ClientUtils {
return { r: r, g: g, b: b, a: a };
}
- const isTransparentFunctionHack = 'isTransparent(__value__)';
+ export const isTransparentFunctionHack = 'isTransparent(__value__)';
export const noRecursionHack = '__noRecursion';
// special case filters
@@ -223,11 +223,6 @@ export namespace ClientUtils {
export function IsRecursiveFilter(val: string) {
return !val.includes(noRecursionHack);
}
- export function HasFunctionFilter(val: string) {
- if (val.includes(isTransparentFunctionHack)) return (color: string) => color !== '' && DashColor(color).alpha() !== 1;
- // add other function filters here...
- return undefined;
- }
export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) {
return 'rgba(' + col.r + ',' + col.g + ',' + col.b + (col.a !== undefined ? ',' + col.a : '') + ')';
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 66c49abc7..03380e4d6 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -5,8 +5,8 @@ enum GPTCallType {
SUMMARY = 'summary',
COMPLETION = 'completion',
EDIT = 'edit',
- CHATCARD = 'chatcard',
- FLASHCARD = 'flashcard',
+ CHATCARD = 'chatcard', // a single flashcard style response to a question
+ FLASHCARD = 'flashcard', // a set of flashcard qustion/answer responses to a topic
QUIZ = 'quiz',
SORT = 'sort',
DESCRIBE = 'describe',
@@ -38,12 +38,11 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
// newest model: gpt-4
summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' },
edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' },
- flashcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Make flashcards out of this text with each question and answer labeled as question and answer. Do not label each flashcard and do not include asterisks: ' },
stack: {
model: 'gpt-4o',
maxTokens: 2048,
temp: 0.7,
- prompt: 'Create a stack of flashcards out of this text with each question and answer labeled as question and answer. For some questions, ask "what is this image of" but tailored to stacks theme and the image and write a keyword that represents the image and label it "keyword". Otherwise, write none. Do not label each flashcard and do not include asterisks.',
+ prompt: 'Create a stack of flashcards out of this text with each question and answer labeled as question and answer. Create a title that represents the question in just a few words and label it "title". For some questions, ask "what is this image of" but tailored to stacks theme and the image and write a keyword that represents the image and label it "keyword". Otherwise, write none. Do not label each flashcard and do not include asterisks.',
},
completion: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." },
@@ -66,6 +65,12 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
prompt: "The user is going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Sort them by the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and NO commas",
},
describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
+ flashcard: {
+ model: 'gpt-4-turbo',
+ maxTokens: 512,
+ temp: 0.5,
+ prompt: 'Make flashcards out of this text with each question and answer labeled as question and answer. Create a title for each question and asnwer that is labeled as "title". Do not label each flashcard and do not include asterisks: ',
+ },
chatcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Answer the following question as a short flashcard response. Do not include a label.' },
quiz: {
model: 'gpt-4-turbo',
@@ -127,7 +132,7 @@ let lastResp = '';
*/
const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: string, dontCache?: boolean) => {
const inputText = [GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZ, GPTCallType.STACK].includes(callType) ? inputTextIn + '.' : inputTextIn;
- const opts: GPTCallOpts = callTypeMap[callType];
+ const opts = callTypeMap[callType];
if (lastCall === inputText && dontCache !== true) return lastResp;
try {
lastCall = inputText;
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 5f54f9d0a..067b9c5e0 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -3,7 +3,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { saveAs } from 'file-saver';
import * as JSZip from 'jszip';
import { action, runInAction } from 'mobx';
-import { ClientUtils } from '../../ClientUtils';
+import { ClientUtils, DashColor } from '../../ClientUtils';
import * as JSZipUtils from '../../JSZipUtils';
import { decycle } from '../../decycler/decycler';
import { DateField } from '../../fields/DateField';
@@ -36,11 +36,16 @@ import { DocumentView } from '../views/nodes/DocumentView';
import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
export namespace DocUtils {
+ function HasFunctionFilter(val: string) {
+ if (val.includes(ClientUtils.isTransparentFunctionHack)) return (d: Doc, color: string) => !d.disableMixBlend && color !== '' && DashColor(color).alpha() !== 1;
+ // add other function filters here...
+ return undefined;
+ }
function matchFieldValue(doc: Doc, key: string, valueIn: unknown): boolean {
let value = valueIn;
- const hasFunctionFilter = ClientUtils.HasFunctionFilter(value as string);
+ const hasFunctionFilter = HasFunctionFilter(value as string);
if (hasFunctionFilter) {
- return hasFunctionFilter(StrCast(doc[key]));
+ return hasFunctionFilter(doc, StrCast(doc[key]));
}
if (key === LinkedTo) {
// links are not a field value, so handled here. value is an expression of form ([field=]idToDoc("..."))
@@ -103,7 +108,6 @@ export namespace DocUtils {
return false;
}
const facetKeys = Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== ClientUtils.noDragDocsFilter.split(Doc.FilterSep)[0]);
- // eslint-disable-next-line no-restricted-syntax
for (const facetKey of facetKeys) {
const facet = filterFacets[facetKey];
@@ -288,7 +292,6 @@ export namespace DocUtils {
return doc;
}
export function AssignDocField(doc: Doc, field: string, creator: (reqdOpts: DocumentOptions, items?: Doc[]) => Doc, reqdOpts: DocumentOptions, items?: Doc[], scripts?: { [key: string]: string }, funcs?: { [key: string]: string }) {
- // eslint-disable-next-line no-return-assign
return DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs);
}
@@ -680,11 +683,19 @@ export namespace DocUtils {
...(defaultTextTemplate
? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance
: {
- _width: width || 200,
- _height: 35,
+ _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200,
+ _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35,
_layout_centered: BoolCast(Doc.UserDoc()._layout_centered),
_layout_fitWidth: true,
_layout_autoHeight: true,
+ backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor),
+ text_fitBox: BoolCast(Doc.UserDoc().fitBox),
+ text_align: StrCast(Doc.UserDoc().textAlign),
+ text_fontColor: StrCast(Doc.UserDoc().fontColor),
+ text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
+ text_fontWeight: StrCast(Doc.UserDoc().fontWeight),
+ text_fontStyle: StrCast(Doc.UserDoc().fontStyle),
+ text_fontDecoration: StrCast(Doc.UserDoc().fontDecoration),
}),
});
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index e79207b04..efe73fbbe 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -48,22 +48,22 @@ export enum DocumentType {
export enum CollectionViewType {
Invalid = 'invalid',
Freeform = 'freeform',
- Schema = 'schema',
- Docking = 'docking',
- Tree = 'tree',
- Stacking = 'stacking',
- Masonry = 'masonry',
- Multicolumn = 'multicolumn',
- Multirow = 'multirow',
- Time = 'time',
+ Calendar = 'calendar',
+ Card = 'card',
Carousel = 'carousel',
Carousel3D = '3D Carousel',
+ Docking = 'docking',
+ Grid = 'grid',
Linear = 'linear',
Map = 'map',
- Grid = 'grid',
+ Masonry = 'masonry',
+ Multicolumn = 'multicolumn',
+ Multirow = 'multirow',
+ NoteTaking = 'notetaking',
Pile = 'pileup',
+ Schema = 'schema',
+ Stacking = 'stacking',
StackedTimeline = 'stacked timeline',
- NoteTaking = 'notetaking',
- Calendar = 'calendar',
- Card = 'card',
+ Time = 'time',
+ Tree = 'tree',
}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b79e8930b..69c66e0dc 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -18,6 +18,7 @@ import { PointData } from '../../pen-gestures/GestureTypes';
import { DocServer } from '../DocServer';
import { dropActionType } from '../util/DropActionTypes';
import { CollectionViewType, DocumentType } from './DocumentTypes';
+import { Id } from '../../fields/FieldSymbols';
class EmptyBox {
public static LayoutString() {
@@ -240,7 +241,6 @@ export class DocumentOptions {
borderWidth?: STRt = new StrInfo('Width of user-added border', false);
borderColor?: STRt = new StrInfo('Color of user-added border', false);
text_fontColor?: STRt = new StrInfo('Color of text', false);
- text_align?: STRt = new StrInfo('alignment');
hCentering?: 'h-left' | 'h-center' | 'h-right';
isDefaultTemplateDoc?: BOOLt = new BoolInfo('');
contentBold?: BOOLt = new BoolInfo('');
@@ -281,7 +281,7 @@ export class DocumentOptions {
_layout_fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)');
_layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition', false);
_layout_enableAltContentUI?: BOOLt = new BoolInfo('whether to show alternate content button');
- _layout_isFlashcard?: BOOLt = new BoolInfo('whether comparison node should be displayed as a flashcard');
+ _layout_flashcardType?: STRt = new StrInfo('flashcard style to render in ComparisonBox. currently just "flashcard".');
_layout_showTitle?: string; // field name to display in header (:hover is an optional suffix)
_layout_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts');
_layout_showCaption?: string; // which field to display in the caption area. leave empty to have no caption
@@ -294,7 +294,6 @@ export class DocumentOptions {
_yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts', false);
_xPadding?: NUMt = new NumInfo('x padding', false);
_yPadding?: NUMt = new NumInfo('y padding', false);
- _singleLine?: boolean; // whether label box is restricted to one line of text
_createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents
_columnWidth?: NUMt = new NumInfo('width of table column', false);
_columnsHideIfEmpty?: BOOLt = new BoolInfo('whether stacking view column headings should be hidden');
@@ -302,10 +301,15 @@ export class DocumentOptions {
_caption_yMargin?: NUMt = new NumInfo('y margin of caption inside of a carousel collection', false, true);
icon_nativeWidth?: NUMt = new NumInfo('native width of icon view', false, true);
icon_nativeHeight?: NUMt = new NumInfo('native height of icon view', false, true);
- _text_fontSize?: string;
- _text_fontFamily?: string;
- _text_fontWeight?: string;
- text_align?: STRt = new StrInfo('horizontal text alignment default');
+ text_fontSize?: string;
+ text_fontFamily?: string;
+ text_fontWeight?: string;
+ text_fitBox?: BOOLt = new BoolInfo("whether text box should be scaled to fit it's containing render box");
+ text_align?: STRt = new StrInfo('horizontal text alignment default', undefined, undefined, ['left', 'center', 'right']);
+ title_align?: STRt = new StrInfo('horizontal title alignment in label box', undefined, undefined, ['left', 'center', 'right']);
+ title_transform?: STRt = new StrInfo('transformation to apply to title in label box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']);
+ text_transform?: STRt = new StrInfo('transformation to apply to text in text box (eg., uppercase)', undefined, undefined, ['uppercase', 'lowercase', 'capitalize']);
+ text_placeholder?: BOOLt = new BoolInfo('makes the text act like a placeholder and automatically select when the text box is selected');
fontSize?: string;
_pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views
@@ -360,6 +364,7 @@ export class DocumentOptions {
isFolder?: BOOLt = new BoolInfo('is document a tree view folder');
_isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label');
_isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target');
+ cloneOnCopy?: BOOLt = new BoolInfo('if this Doc is a field of another Doc, then it should be copied when the other Doc is copied');
mapPin?: DOCt = new DocInfo('pin associated with a config anchor', false);
config_latitude?: NUMt = new NumInfo('latitude of a map', false);
@@ -370,6 +375,8 @@ export class DocumentOptions {
config_panX?: NUMt = new NumInfo('panX saved as a view spec', false);
config_panY?: NUMt = new NumInfo('panY saved as a view spec', false);
config_zoom?: NUMt = new NumInfo('zoom saved as a view spec', false);
+ config_carousel_index?: NUMt = new NumInfo('saved carousel index', false);
+ config_card_curDoc?: DOCt = new DocInfo('current doc in a collection view, e.g., cardView');
config_viewScale?: NUMt = new NumInfo('viewScale saved as a view Spec', false);
presentation_transition?: NUMt = new NumInfo('the time taken for the transition TO a document', false);
presentation_duration?: NUMt = new NumInfo('the duration of the slide in presentation view', false);
@@ -420,6 +427,12 @@ export class DocumentOptions {
flexGap?: NUMt = new NumInfo('Linear view flex gap');
flexDirection?: 'unset' | 'row' | 'column' | 'row-reverse' | 'column-reverse';
+ // Comparison
+ data_revealOp?: STRt = new StrInfo("visual reveal type for front and back of comparison - 'slide' or 'flip' ");
+ data_revealOp_hover?: BOOLt = new BoolInfo('reveal back of comparison manually or by hovering');
+ data_front?: DOCt = new DocInfo('contents of front of flashcard/comparison');
+ data_back?: DOCt = new DocInfo('contents of back of flashcard/comparison');
+
link?: string;
link_description?: string; // added for links
link_relationship?: string; // type of relatinoship a link represents
@@ -486,7 +499,6 @@ export class DocumentOptions {
sidebar_type_collection?: string; // collection type of text sidebar
data_dashboards?: List<FieldType>; // list of dashboards used in shareddocs;
- textTransform?: string;
letterSpacing?: string;
iconTemplate?: string; // name of icon template style
icon_fieldKey?: string; // specifies the icon template to use (e.g., icon_fieldKey='george', then the icon template's name is icon_george; otherwise, the template's name would be icon_<type> where type is the Doc's type(pdf,rich text, etc))
@@ -500,8 +512,8 @@ export class DocumentOptions {
userBackgroundColor?: STRt = new StrInfo('background color associated with a Dash user (seen in header fields of shared documents)');
userColor?: STRt = new StrInfo('color associated with a Dash user (seen in header fields of shared documents)');
- cardSort?: STRt = new StrInfo('way cards are sorted in deck view');
- cardSort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
+ card_sort?: STRt = new StrInfo('way cards are sorted in deck view');
+ card_sort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
}
export const DocOptions = new DocumentOptions();
@@ -697,7 +709,7 @@ export namespace Docs {
dataProps.author_date = new DateField();
if (fieldKey) {
dataProps[`${fieldKey}_modificationDate`] = new DateField();
- dataProps[fieldKey] = options.data ?? data;
+ dataProps[fieldKey] = (options as unknown as { [key: string]: FieldType | undefined })[fieldKey] ?? data;
// so that the list of annotations is already initialised, prevents issues in addonly.
// without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
@@ -784,8 +796,39 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), '', options);
}
- export function ComparisonDocument(text: string, options: DocumentOptions = { title: 'Comparison Box' }) {
- return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), text, options);
+ export function ComparisonDocument(title: string, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', options);
+ }
+ /**
+ * Creates a text box where the supplied text (and optional iimage) will be vertically
+ * and horizontally centered. If text_placeholder is set to true, then the text will be
+ * treated as placeholder text and automatically selected when the text box is selected.
+ * @param title name of text box
+ * @param text text to display in text box
+ * @param opts metadata fields to set on text box
+ * @param img optional image to add to text box
+ * @returns
+ */
+ export function CenteredTextCreator(title: string, text: string, opts: DocumentOptions, img?: Doc) {
+ return TextDocument(RichTextField.textToRtf(text, img?.[Id]), {
+ title, //
+ _layout_autoHeight: true,
+ _layout_centered: true,
+ text_align: 'center',
+ _layout_fitWidth: true,
+ ...opts,
+ });
+ }
+
+ export function FlashcardDocument(title: string, front?: Doc, back?: Doc, options: DocumentOptions = { title: 'Flashcard' }) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', {
+ data_front: front ?? CenteredTextCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards', { text_placeholder: true, cloneOnCopy: true }, undefined),
+ data_back: back ?? CenteredTextCreator('answer', 'answer here', { text_placeholder: true, cloneOnCopy: true }, undefined),
+ _layout_fitWidth: true,
+ _layout_flashcardType: 'flashcard',
+ title,
+ ...options,
+ });
}
export function DiagramDocument(options: DocumentOptions = { title: '' }) {
return InstanceFromProto(Prototypes.get(DocumentType.DIAGRAM), undefined, options);
@@ -827,7 +870,7 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.MESSAGE), field, options, undefined, fieldKey);
}
- export function TextDocument(text: string, options: DocumentOptions = {}, fieldKey: string = 'text') {
+ export function TextDocument(text: string | RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') {
const rtf = {
doc: {
type: 'doc',
@@ -846,7 +889,7 @@ export namespace Docs {
selection: { type: 'text', anchor: 1, head: 1 },
storedMarks: [],
};
- const field = text ? new RichTextField(JSON.stringify(rtf), text) : undefined;
+ const field = text instanceof RichTextField ? text : text ? new RichTextField(JSON.stringify(rtf), text) : options.text instanceof RichTextField ? options.text : undefined;
return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
}
@@ -884,7 +927,6 @@ export namespace Docs {
I.text_align = 'center';
I.rotation = 0;
I.defaultDoubleClick = 'ignore';
- I.keepZWhenDragged = true;
I.author_date = new DateField();
I.acl_Guest = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View;
// I.acl_Override = SharingPermissions.Unset;
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index bb3d232fb..009cd7de7 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -4,7 +4,7 @@ import * as rp from 'request-promise';
import { ClientUtils, OmitKeys } from "../../ClientUtils";
import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from "../../fields/Doc";
import { DocData } from "../../fields/DocSymbols";
-import { InkTool } from "../../fields/InkField";
+import { InkEraserTool, InkInkTool, InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
import { RichTextField } from "../../fields/RichTextField";
@@ -39,6 +39,7 @@ import { SelectionManager } from "./SelectionManager";
import { ColorScheme } from "./SettingsManager";
import { SnappingManager } from "./SnappingManager";
import { UndoManager } from "./UndoManager";
+import { DocumentView } from "../views/nodes/DocumentView";
export interface Button {
// DocumentOptions fields a button can set
@@ -88,7 +89,7 @@ export class CurrentUserUtils {
const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, isSystem: true};
const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
{ opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCastAsync(documentView?.containerViewPath().lastElement()?.Document.target).then((target) => target && (target.proto.data = new List([this])))"},
- { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: `openDoc(this.doubleClickView.${OpenWhere.addRight})`}];
+ { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: `openDoc(this.doubleClickView, "${OpenWhere.addRight}")`}];
const reqdClickList = reqdTempOpts.map(opts => {
const allOpts = {...reqdClickOpts, ...opts.opts};
const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(fdoc => fdoc.title === opts.opts.title): undefined;
@@ -186,7 +187,7 @@ export class CurrentUserUtils {
const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts));
const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({
- layout: LabelBox.LayoutString(fieldKey), textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
+ layout: LabelBox.LayoutString(fieldKey), letterSpacing: "unset", _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
});
const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts });
const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts });
@@ -268,7 +269,7 @@ export class CurrentUserUtils {
MakeTemplate(Docs.Create.MultirowDocument(
[
Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200, isSystem: true }),
- Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) })
+ Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, text_fontFamily: StrCast(Doc.UserDoc().fontFamily), text_fontSize: StrCast(Doc.UserDoc().fontSize) })
], {...opts, title: "Slide View Template"}));
const plotlyApi = () => {
let plotly = Doc.MyPublishedDocs.find(fdoc => fdoc.title === "@plotly");
@@ -369,14 +370,14 @@ pie title Minerals in my tap water
creator:(opts:DocumentOptions)=> Doc // how to create the empty thing if it doesn't exist
}[] = [
{key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }},
- {key: "Flashcard", creator: opts => Docs.Create.ComparisonDocument("", opts), opts: { _layout_isFlashcard: true, _layout_fitWidth: true, _width: 300, _height: 300}},
+ {key: "Flashcard", creator: opts => Docs.Create.FlashcardDocument("", undefined, undefined, opts),opts: { _width: 300, _height: 300}},
{key: "Image", creator: opts => Docs.Create.ImageDocument("", opts), opts: { _width: 400, _height:400 }},
{key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 300, _height: 35, }},
{key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}},
{key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, }},
{key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }},
{key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, data_useCors: true, }},
- {key: "Comparison", creator: opts => Docs.Create.ComparisonDocument("",opts), opts: { _width: 300, _height: 300 }},
+ {key: "Comparison", creator: opts => Docs.Create.ComparisonDocument("", opts), opts: { _width: 300, _height: 300 }},
{key: "Diagram", creator: Docs.Create.DiagramDocument, opts: { _width: 300, _height: 300, _type_collection: CollectionViewType.Freeform, layout_diagramEditor: CollectionView.LayoutString("data") }, scripts: { onPaint: `toggleDetail(documentView, "diagramEditor","")`}},
{key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }},
{key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, _layout_fitWidth: true, }},
@@ -392,7 +393,7 @@ pie title Minerals in my tap water
{key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, _layout_dontCenter:'xy', dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }},
{key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }},
{key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree,
- treeView_HasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true,
+ treeView_HasOverlay: true, text_fontSize: "20px", _layout_autoHeight: true,
dropAction:dropActionType.move, treeView_Type: TreeViewType.outline,
backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true
}, funcs: {title: 'this.text?.Text'}},
@@ -731,11 +732,11 @@ pie title Minerals in my tap water
return [
{ title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'},
btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
- { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 },
+ { title: " Size", toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 9 },
{ title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
{ title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
{ title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
- { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
+ { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italic", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
@@ -746,6 +747,7 @@ pie title Minerals in my tap water
{ title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
]},
+ { title: "Fit Box", toolTip: "Fit text to box", btnType: ButtonType.ToggleButton, icon: "object-group",toolType:"fitBox", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Elide", toolTip: "Elide selection", btnType: ButtonType.ToggleButton, icon: "eye", toolType:"elide", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
@@ -757,24 +759,27 @@ pie title Minerals in my tap water
static inkTools():Button[] {
return [
- { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
- { title: "Highlight",toolTip: "Highlight (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter",toolType: "highlighter", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
- { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
- { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' },
+ { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType: Gestures.Circle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+ { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType: Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+ { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType: Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
+ { title: "Ink", toolTip: "Ink", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Ink, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' },
+ subMenu: [
+ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: InkInkTool.Pen, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
+ { title: "Highlight",toolTip: "Highlight (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", toolType: InkInkTool.Highlight, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }},
+ { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: InkInkTool.Write, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
+ ]},
+ { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}, numBtnMin: 1, linearBtnWidth:40},
+ { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}},
+ { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' },
subMenu: [
- { title: "Stroke", toolTip: "Stroke Erase", btnType: ButtonType.ToggleButton, icon: "eraser", toolType:InkTool.StrokeEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
- { title: "Segment", toolTip: "Segment Erase", btnType: ButtonType.ToggleButton, icon: "xmark", toolType:InkTool.SegmentEraser,ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
- { title: "Radius", toolTip: "Radius Erase", btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkTool.RadiusEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
+ { title: "Stroke", toolTip: "Eraser complete strokes",btnType: ButtonType.ToggleButton, icon: "eraser", toolType:InkEraserTool.Stroke, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
+ { title: "Segment", toolTip: "Erase segments up to intersections",btnType: ButtonType.ToggleButton,icon: "xmark",toolType:InkEraserTool.Segment,ignoreClick:true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
+ { title: "Area", toolTip: "Erase like a pencil", btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkEraserTool.Radius, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}},
]},
- { title: "Eraser Width", toolTip: "Eraser Width", btnType: ButtonType.NumberSliderButton, toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1, funcs: {hidden:"NotRadiusEraser()"}},
- { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType: Gestures.Circle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
- { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType: Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
- { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType: Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
- { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
- { title: "Labels", toolTip: "Labels", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
- { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
- { title: "Ink", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} },
- { title: "Smart Draw", toolTip: "Draw with GPT", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: "smartdraw", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}},
+ { title: " Size", toolTip: "Size of area pencil eraser", btnType: ButtonType.NumberSliderButton, toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"NotRadiusEraser()"}, numBtnMin: 1, linearBtnWidth:40},
+ { title: "Mask", toolTip: "Make Stroke a Stencil Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
+ { title: "Labels", toolTip: "Show Labels Inside Shapes", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}},
+ { title: "Smart Draw", toolTip: "Draw with GPT", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: "smartdraw", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}},
];
}
@@ -782,7 +787,7 @@ pie title Minerals in my tap water
return [
{title: "Preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, icon: "portrait", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'} },
{title: "1 Line", toolTip: "Single Line Rows", btnType: ButtonType.ToggleButton, icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} },
- {title: "DataViz", toolTip: "Turn Schema Table into Data Visualization Doc", btnType: ButtonType.ClickButton, icon: "chart-bar", scripts:{ onClick: '{ datavizFromSchema()'} }, ];
+ {title: "DataViz", toolTip: "Turn Schema Table into Data Visualization Doc", btnType: ButtonType.ClickButton, icon: "chart-bar", scripts:{ onClick: 'datavizFromSchema()'} }, ];
}
static webTools() {
@@ -805,20 +810,19 @@ pie title Minerals in my tap water
}
static contextMenuTools(doc:Doc):Button[] {
return [
- { btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree,
- CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn,
- CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel,
- CollectionViewType.Carousel3D, CollectionViewType.Card, CollectionViewType.Linear, CollectionViewType.Map,
- CollectionViewType.Calendar, CollectionViewType.Grid, CollectionViewType.NoteTaking, ]),
+ { btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Card, CollectionViewType.Carousel,CollectionViewType.Carousel3D,
+ CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Linear,
+ CollectionViewType.Map, CollectionViewType.NoteTaking, CollectionViewType.Schema, CollectionViewType.Stacking,
+ CollectionViewType.Calendar, CollectionViewType.Grid, CollectionViewType.Tree, CollectionViewType.Time, ]),
title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: '{ return setView(value, _readOnly_); }'}},
{ title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}},
{ title: "Header", icon: "heading", toolTip: "Doc Titlebar Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} },
{ title: "Template",icon: "scroll", toolTip: "Default Note Template",btnType: ButtonType.ToggleButton, expertMode: false, toolType:DocumentType.RTF, scripts: { onClick: '{ return setDefaultTemplate(_readOnly_); }'} },
- { title: "Fill", icon: "fill-drip", toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected
- { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
- { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
+ { title: "Fill", icon: "fill-drip", toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'} }, // Only when a document is selected
+ { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
+ { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
{ title: "Num", icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
- { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
+ { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
{ title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
{ title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
@@ -863,7 +867,7 @@ pie title Minerals in my tap water
}
// linear view
const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]),
- childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: !params.scripts?.onClick,
+ childDontRegisterViews: true, flexGap: 0, _height: 30, _width: 30, ignoreClick: !params.scripts?.onClick,
linearView_SubMenu: true, linearView_Expandable: true, embedContainer: menuDoc};
const items = (menutBtn?:Doc) => !menutBtn ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menutBtn) );
@@ -987,15 +991,23 @@ pie title Minerals in my tap water
Doc.noviceMode ?? (Doc.noviceMode = true);
doc._showLabel ?? (doc._showLabel = true);
doc.textAlign ?? (doc.textAlign = "left");
+ doc.textBackgroundColor ?? (doc.textBackgroundColor = Colors.LIGHT_GRAY);
doc.activeTool = InkTool.None;
doc.openInkInLightbox ?? (doc.openInkInLightbox = false);
- doc.activeInkHideTextLabels ?? (doc.activeInkHideTextLabels = false);
- doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");
- doc.activeInkWidth ?? (doc.activeInkWidth = 1);
- doc.activeInkBezier ?? (doc.activeInkBezier = "0");
- doc.activeFillColor ?? (doc.activeFillColor = "");
- doc.activeArrowStart ?? (doc.activeArrowStart = "");
- doc.activeArrowEnd ?? (doc.activeArrowEnd = "");
+ doc.activeHideTextLabels ?? (doc.activeHideTextLabels = false);
+ doc[`active${InkInkTool.Pen}Color`] ?? (doc[`active${InkInkTool.Pen}Color`] = "rgb(0, 0, 0)");
+ doc[`active${InkInkTool.Pen}Width`] ?? (doc[`active${InkInkTool.Pen}Width`] = 2);
+ doc[`active${InkInkTool.Pen}Bezier`] ?? (doc[`active${InkInkTool.Pen}Bezier`] = "0");
+ doc[`active${InkInkTool.Write}Color`] ?? (doc[`active${InkInkTool.Write}Color`] = "rgb(255, 0, 0)");
+ doc[`active${InkInkTool.Write}Width`] ?? (doc[`active${InkInkTool.Write}Width`] = 1);
+ doc[`active${InkInkTool.Write}Bezier`] ?? (doc[`active${InkInkTool.Write}Bezier`] = "0");
+ doc[`active${InkInkTool.Highlight}Color`] ?? (doc[`active${InkInkTool.Highlight}Color`] = 'transparent');
+ doc[`active${InkInkTool.Highlight}Width`] ?? (doc[`active${InkInkTool.Highlight}Width`] = 20);
+ doc[`active${InkInkTool.Highlight}Bezier`] ?? (doc[`active${InkInkTool.Highlight}Bezier`] = "0");
+ doc[`active${InkInkTool.Highlight}Fill`] ?? (doc[`active${InkInkTool.Highlight}Fill`] = "rgba(0, 255, 255, 0.4)");
+ doc.activeInkTool ?? (doc.activeInkTool = InkInkTool.Pen);
+ doc.activeEraserTool ?? (doc.activeEraserTool = InkEraserTool.Stroke);
+ doc.activeEraserWidth ?? (doc.activeEraserWidth = 20);
doc.activeDash ?? (doc.activeDash === "0");
doc.fontSize ?? (doc.fontSize = "12px");
doc.fontFamily ?? (doc.fontFamily = "Arial");
@@ -1130,7 +1142,9 @@ pie title Minerals in my tap water
}
// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.RadiusEraser; }, "is the active tool anything but the radius eraser");
+ScriptingGlobals.add(function activeInkTool() { return Doc.ActiveTool=== InkTool.Ink || DocumentView.Selected().some(dv => dv.layoutDoc.layout_isSvg); }, "is a pen tool or an ink stroke active");
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.Eraser || Doc.ActiveEraser !== InkEraserTool.Radius; }, "is the active tool anything but the radius eraser");
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs");
// eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 4ab2e8d05..286112bf9 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -256,7 +256,11 @@ export class DocumentManager {
Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc);
const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
- if (DocumentView.activateTabView(docContextPath[0])) options.toggleTarget = false;
+ let activatedTab = false;
+ if (DocumentView.activateTabView(docContextPath[0])) {
+ options.toggleTarget = false;
+ activatedTab = true;
+ }
const rootContextView =
docContextPath.length &&
@@ -268,7 +272,7 @@ export class DocumentManager {
return;
}
options.didMove = true;
- (!DocumentView.LightboxDoc() && docContextPath.some(doc => DocumentView.activateTabView(doc))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
+ (!DocumentView.LightboxDoc() && (activatedTab || docContextPath.some(doc => DocumentView.activateTabView(doc)))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
}));
if (options.openLocation?.includes(OpenWhere.lightbox)) {
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 4231c2ca8..ca1cb8014 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -109,7 +109,7 @@ export namespace InteractionUtils {
dash: string | undefined,
scalexIn: number,
scaleyIn: number,
- shape: Gestures,
+ shape: Gestures | undefined,
pevents: Property.PointerEvents,
opacity: number,
nodefs: boolean,
@@ -136,7 +136,7 @@ export namespace InteractionUtils {
const arrowLengthFactor = 5 * (markerScale || 0.5);
const arrowNotchFactor = 2 * (markerScale || 0.5);
return (
- <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
+ <svg fill={color || 'transparent'} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
{' '}
{/* setting the svg fill sets the arrowStart fill */}
{nodefs ? null : (
@@ -186,7 +186,7 @@ export namespace InteractionUtils {
opacity: 1.0,
// opacity: strokeWidth !== width ? 0.5 : undefined,
pointerEvents: pevents === 'all' ? 'visiblePainted' : pevents,
- stroke: color ?? 'rgb(0, 0, 0)',
+ stroke: (color ?? 'rgb(0, 0, 0)') || 'transparent',
strokeWidth,
strokeLinecap: strokeLineCap,
strokeDasharray: dashArray,
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 0a3a0ba49..0e67dcfaa 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -113,12 +113,10 @@ export class LinkFollower {
}
const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)];
if (srcAnchor.followLinkXoffset !== undefined && moveTo[0] !== target.x) {
- // eslint-disable-next-line prefer-destructuring
target.x = moveTo[0];
movedTarget = true;
}
if (srcAnchor.followLinkYoffset !== undefined && moveTo[1] !== target.y) {
- // eslint-disable-next-line prefer-destructuring
target.y = moveTo[1];
movedTarget = true;
}
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index b1db0bf39..5d78c2fab 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -48,7 +48,7 @@ export function isCompileError(toBeDetermined: CompileResult): toBeDetermined is
// eslint-disable-next-line no-use-before-define
function Run(script: string | undefined, customParams: string[], diagnostics: ts.Diagnostic[], originalScript: string, options: ScriptOptions): CompileResult {
- const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error);
+ const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error).filter(diag => diag.code !== 2304 && diag.code !== 2339);
if ((options.typecheck !== false && errors.length) || !script) {
return { compiled: false, errors };
}
@@ -188,6 +188,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
}, '');
const found = ScriptField.GetScriptFieldCache(script + ':' + signature); // if already compiled, found is the result; cache set below
if (found) return found as CompiledScript;
+ options.typecheck = true;
const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
if (options.params && !options.params.this) options.params.this = Doc.name;
if (options.globals) {
@@ -221,13 +222,10 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
if ('this' in params || 'this' in capturedVariables) {
paramNames.push('this');
}
- for (const key in params) {
- if (key !== 'this') {
- paramNames.push(key);
- }
- }
+ paramNames.push(...Object.keys(params).filter(p => p!== 'this' && !Object.keys(capturedVariables).includes(p)));
+
const paramList = paramNames.map(key => {
- const val = params[key];
+ const val = typeof params[key] === "string" && params[key].length && !"\"'`".includes(params[key][0]) ? `"${params[key]}"` : params[key];
return `${key}: ${val}`;
});
for (const key in capturedVariables) {
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 9200d68db..628097ca8 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components';
+import { Button, ColorPicker, Colors, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components';
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -45,7 +45,6 @@ export class SettingsManager extends React.Component<object> {
public closeMgr = action(() => {
this._isOpen = false;
});
- // eslint-disable-next-line react/no-unused-class-component-methods
public openMgr = action(() => {
this._isOpen = true;
});
@@ -269,9 +268,9 @@ export class SettingsManager extends React.Component<object> {
formLabelPlacement="right"
toggleType={ToggleType.SWITCH}
onClick={() => {
- Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels;
+ Doc.UserDoc().activeHideTextLabels = !Doc.UserDoc().activeHideTextLabels;
}}
- toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)}
+ toggleStatus={BoolCast(Doc.UserDoc().activeHideTextLabels)}
size={Size.XSMALL}
color={SettingsManager.userColor}
/>
@@ -354,6 +353,27 @@ export class SettingsManager extends React.Component<object> {
Doc.UserDoc().fontSize = val + 'px';
}}
/>
+ <ColorPicker
+ color={SettingsManager.userColor}
+ type={Type.PRIM}
+ defaultPickerType="Classic"
+ selectedColor={StrCast(Doc.UserDoc().textBackgroundColor, Colors.LIGHT_GRAY)}
+ background={SnappingManager.userBackgroundColor}
+ icon={<FontAwesomeIcon icon="palette" size="lg" />}
+ tooltip="default text background color"
+ label="background"
+ setSelectedColor={value => {
+ Doc.UserDoc().textBackgroundColor = value;
+ // if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
+ // this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+ }}
+ setFinalColor={value => {
+ Doc.UserDoc().textBackgroundColor = value;
+ // this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+ // this.colorBatch?.end();
+ // this.colorBatch = undefined;
+ }}
+ />
<Dropdown
items={fontFamilies.map(val => ({
text: val,
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 5f6c7d9ac..2a150dc5a 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -1,4 +1,5 @@
import { observable, action, runInAction, makeObservable } from 'mobx';
+import { Gestures } from '../../pen-gestures/GestureTypes';
export enum freeformScrollMode {
Pan = 'pan',
@@ -29,6 +30,8 @@ export class SnappingManager {
@observable _propertyWid: number = 0;
@observable _printToConsole: boolean = false;
@observable _hideDecorations: boolean = false;
+ @observable _keepGestureMode: boolean = false; // for whether primitive selection enters a one-shot or persistent mode
+ @observable _inkShape: Gestures | undefined = undefined;
private constructor() {
SnappingManager._manager = this;
@@ -61,6 +64,8 @@ export class SnappingManager {
public static get PropertiesWidth(){ return this.Instance._propertyWid; } // prettier-ignore
public static get PrintToConsole() { return this.Instance._printToConsole; } // prettier-ignore
public static get HideDecorations(){ return this.Instance._hideDecorations; } // prettier-ignore
+ public static get KeepGestureMode(){ return this.Instance._keepGestureMode; } // prettier-ignore
+ public static get InkShape() { return this.Instance._inkShape; } // prettier-ignore
public static SetLongPress = (press: boolean) => runInAction(() => {this.Instance._longPress = press}); // prettier-ignore
public static SetShiftKey = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore
@@ -78,6 +83,8 @@ export class SnappingManager {
public static SetPropertiesWidth= (wid:number) =>runInAction(() => {this.Instance._propertyWid = wid}); // prettier-ignore
public static SetPrintToConsole = (state:boolean) =>runInAction(() => {this.Instance._printToConsole = state}); // prettier-ignore
public static SetHideDecorations= (state:boolean) =>runInAction(() => {this.Instance._hideDecorations = state}); // prettier-ignore
+ public static SetKeepGestureMode= (state:boolean) =>runInAction(() => {this.Instance._keepGestureMode = state}); // prettier-ignore
+ public static SetInkShape = (shape?:Gestures)=>runInAction(() => {this.Instance._inkShape = shape}); // prettier-ignore
public static userColor: string | undefined;
public static userVariantColor: string | undefined;
diff --git a/src/client/util/node_modules/type_decls.d b/src/client/util/node_modules/type_decls.d
index 1a93bbe59..9058b4394 100644
--- a/src/client/util/node_modules/type_decls.d
+++ b/src/client/util/node_modules/type_decls.d
@@ -198,6 +198,9 @@ declare class InkField extends ObjectField {
constructor(data:Array<{X:number, Y:number}>);
[Copy](): ObjectField;
}
+declare class DocumentDragData {
+ constructor(dragDoc: Doc[], dropAction?: string);
+}
// @ts-ignore
declare const console: any;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 24ebca751..45dfcb5b8 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -10,7 +10,7 @@ import { lightOrDark, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
import { Utils, emptyFunction, numberValue } from '../../Utils';
import { DateField } from '../../fields/DateField';
import { Doc, DocListCast, Field, FieldType, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
-import { AclAdmin, AclAugment, AclEdit, DocData } from '../../fields/DocSymbols';
+import { AclAdmin, AclAugment, AclEdit, Animation, DocData } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { InkField } from '../../fields/InkField';
import { ScriptField } from '../../fields/ScriptField';
@@ -650,7 +650,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
this._forceRender;
const { b, r, x, y } = this.Bounds;
const seldocview = DocumentView.Selected().lastElement();
- if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || SnappingManager.HideDecorations || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
+ if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || seldocview?.Document[Animation] || SnappingManager.HideDecorations || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
setTimeout(
action(() => {
this._editingTitle = false;
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 11425e477..99738052d 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -102,7 +102,7 @@ const HotKeyIconButton: React.FC<HotKeyButtonProps> = observer(({ hotKey /*, sel
return (
<div
- className={`filterHotKey-button`}
+ className="filterHotKey-button"
onClick={e => {
e.stopPropagation();
state.startEditing();
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index afeecaa63..2d6cd03e0 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -2,9 +2,9 @@ import * as fitCurve from 'fit-curve';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
+import { setupMoveUpEvents } from '../../ClientUtils';
import { emptyFunction, intersectRect } from '../../Utils';
-import { Doc, Opt, returnEmptyDoclist } from '../../fields/Doc';
+import { Doc } from '../../fields/Doc';
import { InkData, InkField, InkTool } from '../../fields/InkField';
import { NumCast } from '../../fields/Types';
import { Gestures } from '../../pen-gestures/GestureTypes';
@@ -14,27 +14,25 @@ import { DocumentType } from '../documents/DocumentTypes';
import { Docs } from '../documents/Documents';
import { InteractionUtils } from '../util/InteractionUtils';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { Transform } from '../util/Transform';
+import { SnappingManager } from '../util/SnappingManager';
import { undoable } from '../util/UndoManager';
import './GestureOverlay.scss';
import { InkingStroke } from './InkingStroke';
import { ObservableReactComponent } from './ObservableReactComponent';
-import { returnEmptyDocViewList } from './StyleProvider';
import { CollectionFreeFormView } from './collections/collectionFreeForm';
import {
- ActiveArrowEnd,
- ActiveArrowScale,
- ActiveArrowStart,
- ActiveDash,
- ActiveFillColor,
- ActiveInkBezierApprox,
+ ActiveInkArrowEnd,
+ ActiveInkArrowScale,
+ ActiveInkArrowStart,
ActiveInkColor,
+ ActiveInkDash,
+ ActiveInkFillColor,
ActiveInkWidth,
DocumentView,
- SetActiveArrowStart,
- SetActiveDash,
- SetActiveFillColor,
+ SetActiveInkArrowStart,
SetActiveInkColor,
+ SetActiveInkDash,
+ SetActiveInkFillColor,
SetActiveInkWidth,
} from './nodes/DocumentView';
export enum ToolglassTools {
@@ -57,18 +55,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
// eslint-disable-next-line no-use-before-define
static Instances: GestureOverlay[] = [];
- @observable public InkShape: Opt<Gestures> = undefined;
@observable public SavedColor?: string = undefined;
@observable public SavedWidth?: number = undefined;
@observable public Tool: ToolglassTools = ToolglassTools.None;
- @observable public KeepPrimitiveMode = false; // for whether primitive selection enters a one-shot or persistent mode
@observable private _thumbX?: number = undefined;
@observable private _thumbY?: number = undefined;
@observable private _pointerY?: number = undefined;
@observable private _points: { X: number; Y: number }[] = [];
- @observable private _strokes: InkData[] = [];
- @observable private _palette?: JSX.Element = undefined;
@observable private _clipboardDoc?: JSX.Element = undefined;
@computed private get height(): number {
@@ -95,8 +89,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
}
@action
onPointerDown = (e: React.PointerEvent) => {
+ (document.activeElement as HTMLElement)?.blur();
if (!(e.target as HTMLElement)?.className?.toString().startsWith('lm_')) {
- if ([InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (Doc.ActiveTool === InkTool.Ink) {
this._points.push({ X: e.clientX, Y: e.clientY });
setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
}
@@ -122,13 +117,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
};
@action primCreated() {
- if (!this.KeepPrimitiveMode) {
- this.InkShape = undefined;
- // get out of ink mode after each stroke=
- // if (Doc.ActiveTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor);
+ if (!SnappingManager.KeepGestureMode) {
+ SnappingManager.SetInkShape(undefined);
Doc.ActiveTool = InkTool.None;
- // SetActiveArrowStart('none');
- // SetActiveArrowEnd('none');
}
}
/**
@@ -244,15 +235,16 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
this.dispatchGesture(Gestures.Stroke);
};
@action
- onPointerUp = () => {
+ onPointerUp = (e: PointerEvent) => {
const ffView = DocumentView.DownDocView?.ComponentView instanceof CollectionFreeFormView && DocumentView.DownDocView.ComponentView;
+ const downView = DocumentView.DownDocView;
DocumentView.DownDocView = undefined;
if (this._points.length > 1) {
const B = this.svgBounds;
const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
const { Name, Score } =
- (this.InkShape
- ? new Result(this.InkShape, 1, Date.now)
+ (SnappingManager.InkShape
+ ? new Result(SnappingManager.InkShape, 1, Date.now)
: Doc.UserDoc().recognizeGestures && points.length > 2
? GestureUtils.GestureRecognizer.Recognize([points])
: undefined) ??
@@ -286,7 +278,11 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
this.dryInk();
}
}
+ } else {
+ (downView?.ComponentView as CollectionFreeFormView)?._marqueeViewRef?.current?.setPreviewCursor?.(this._points[0].X, this._points[0].Y, false, false, undefined);
+ e.preventDefault();
}
+ this.primCreated();
this._points.length = 0;
};
/**
@@ -552,97 +548,38 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
B.bottom += width / 2;
B.width += width;
B.height += width;
- const fillColor = ActiveFillColor();
+ const fillColor = ActiveInkFillColor();
const strokeColor = fillColor && fillColor !== 'transparent' ? fillColor : ActiveInkColor();
return [
this.props.children,
- this._palette,
- [
- this._strokes.map((l, i) => {
- const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(l, true);
- return (
- <svg key={i} width={b.width} height={b.height} style={{ top: 0, left: 0, transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
- {InteractionUtils.CreatePolyline(
- l,
- b.left,
- b.top,
- strokeColor,
- width,
- width,
- 'miter',
- 'round',
- ActiveInkBezierApprox(),
- 'none' /* ActiveFillColor() */,
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveArrowScale(),
- ActiveDash(),
- 1,
- 1,
- this.InkShape as Gestures,
- 'none',
- 1.0,
- false
- )}
- </svg>
- );
- }),
- this._points.length <= 1 ? null : (
- <svg key="svg" width={B.width} height={B.height} style={{ top: 0, left: 0, transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
- {InteractionUtils.CreatePolyline(
- this._points.map(p => ({ X: p.X - (rect?.x || 0), Y: p.Y - (rect?.y || 0) })),
- B.left,
- B.top,
- ActiveInkColor(),
- width,
- width,
- 'miter',
- 'round',
- '',
- 'none' /* ActiveFillColor() */,
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveArrowScale(),
- ActiveDash(),
- 1,
- 1,
- this.InkShape as Gestures,
- 'none',
- 1.0,
- false
- )}
- </svg>
- ),
- ],
+ this._points.length <= 1 ? null : (
+ <svg key="svg" width={B.width} height={B.height} style={{ top: 0, left: 0, transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
+ {InteractionUtils.CreatePolyline(
+ this._points.map(p => ({ X: p.X - (rect?.x || 0), Y: p.Y - (rect?.y || 0) })),
+ B.left,
+ B.top,
+ strokeColor,
+ width,
+ width,
+ 'miter',
+ 'round',
+ '',
+ 'none' /* ActiveFillColor() */,
+ ActiveInkArrowStart(),
+ ActiveInkArrowEnd(),
+ ActiveInkArrowScale(),
+ ActiveInkDash(),
+ 1,
+ 1,
+ SnappingManager.InkShape,
+ 'none',
+ 1.0,
+ false
+ )}
+ </svg>
+ ),
];
}
- screenToLocalTransform = () => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + this.height, 1);
- return300 = () => 300;
- @action
- public openFloatingDoc = (doc: Doc) => {
- this._clipboardDoc = (
- <DocumentView
- Document={doc}
- addDocument={undefined}
- addDocTab={returnFalse}
- pinToPres={emptyFunction}
- removeDocument={undefined}
- ScreenToLocalTransform={this.screenToLocalTransform}
- PanelWidth={this.return300}
- PanelHeight={this.return300}
- isDocumentActive={returnFalse}
- isContentActive={returnFalse}
- renderDepth={0}
- styleProvider={returnEmptyString}
- containerViewPath={returnEmptyDocViewList}
- focus={emptyFunction}
- whenChildContentsActiveChanged={emptyFunction}
- childFiltersByRanges={returnEmptyFilter}
- childFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- />
- );
- };
@action
public closeFloatingDoc = () => {
@@ -689,10 +626,10 @@ ScriptingGlobals.add(function setPen(width: string, color: string, fill: string,
SetActiveInkColor(color);
GestureOverlay.Instance.SavedWidth = ActiveInkWidth();
SetActiveInkWidth(width);
- SetActiveFillColor(fill);
- SetActiveArrowStart(arrowStart);
- SetActiveArrowStart(arrowEnd);
- SetActiveDash(dash);
+ SetActiveInkFillColor(fill);
+ SetActiveInkArrowStart(arrowStart);
+ SetActiveInkArrowStart(arrowEnd);
+ SetActiveInkDash(dash);
});
});
// eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d7d8e9506..f16338361 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -2,7 +2,7 @@ import { random } from 'lodash';
import { action } from 'mobx';
import { Doc, DocListCast } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
-import { InkTool } from '../../fields/InkField';
+import { InkInkTool, InkTool } from '../../fields/InkField';
import { ScriptField } from '../../fields/ScriptField';
import { Cast, PromiseValue } from '../../fields/Types';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
@@ -280,10 +280,10 @@ export class KeyManager {
}
break;
case 'e':
- Doc.ActiveTool = [InkTool.StrokeEraser, InkTool.SegmentEraser, InkTool.RadiusEraser].includes(Doc.ActiveTool) ? InkTool.None : InkTool.StrokeEraser;
+ Doc.ActiveTool = Doc.ActiveTool === InkTool.Eraser ? InkTool.None : InkTool.Eraser;
break;
case 'p':
- Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+ Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
break;
case 'r':
preventDefault = false;
@@ -378,7 +378,8 @@ export class KeyManager {
UndoManager.Redo();
break;
case 'p':
- Doc.ActiveTool = InkTool.Write;
+ Doc.ActiveInk = InkInkTool.Write;
+ Doc.ActiveTool = InkTool.Ink;
break;
default:
}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 270266a94..5199eb02b 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -470,14 +470,13 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
className="inkStroke"
style={{
transform: isInkMask ? `rotate(-${NumCast(this._props.LocalRotation?.() ?? 0)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
- // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
cursor: this._props.isSelected() ? 'default' : undefined,
}}
{...interactions}>
{clickableLine(this.onPointerDown, isInkMask)}
{isInkMask ? null : inkLine}
</svg>
- {!closed || this.dataDoc[this.fieldKey + '_showLabel'] === false || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : (
+ {!closed || this.dataDoc[this.fieldKey + '_showLabel'] === false || (!RTFCast(this.dataDoc.text)?.Text && !this.dataDoc[this.fieldKey + '_showLabel'] && (!this._props.isSelected() || Doc.UserDoc().activeHideTextLabels)) ? null : (
<div
className="inkStroke-text"
style={{
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index f55ed2aa0..65de85008 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -214,7 +214,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
// if (this._showPalette === false) AnnotationPalette.Instance.resetPalette(true);
};
togglePen = () => {
- Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+ Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
};
toggleExplore = () => SnappingManager.SetExploreMode(!SnappingManager.ExploreMode);
@@ -332,7 +332,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
{toggleBtn('lightboxView-navBtn', 'toggle reading view', BoolCast(this._doc?._layout_fitWidth), 'book-open', 'book', this.toggleFitWidth)}
{toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-export', '', this.downloadDoc)}
{toggleBtn('lightboxView-paletteBtn', 'toggle sticker palette', this._showPalette === true, 'palette', '', this.togglePalette)}
- {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)}
+ {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Ink, 'pen', '', this.togglePen)}
{toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)}
</div>
);
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 7266875c5..fa1123a2d 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -34,7 +34,7 @@ export interface MarqueeAnnotatorProps {
getPageFromScroll?: (top: number) => number;
finishMarquee: (x?: number, y?: number) => void;
anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
- anchorMenuFlashcard?: () => Promise<String>;
+ anchorMenuFlashcard?: () => Promise<string>;
anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined;
highlightDragSrcColor?: string;
}
@@ -220,7 +220,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
e.preventDefault();
e.stopPropagation();
let cropRegion: Doc | undefined;
- // eslint-disable-next-line no-return-assign
const sourceAnchorCreator = () => (cropRegion = this.highlight('', true, undefined, true)); // hyperlink color
const targetCreator = (/* annotationOn: Doc | undefined */) => this.props.anchorMenuCrop!(cropRegion, false)!;
DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
diff --git a/src/client/views/PinFuncs.ts b/src/client/views/PinFuncs.ts
index 430455644..ab02c2d07 100644
--- a/src/client/views/PinFuncs.ts
+++ b/src/client/views/PinFuncs.ts
@@ -1,4 +1,4 @@
-import { Doc, DocListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
import { Copy, Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
@@ -16,7 +16,7 @@ export interface pinDataTypes {
scrollable?: boolean;
dataviz?: number[];
pannable?: boolean;
- type_collection?: boolean;
+ collectionType?: boolean;
inkable?: boolean;
filters?: boolean;
pivot?: boolean;
@@ -39,9 +39,14 @@ export interface PinProps {
pinData?: pinDataTypes;
}
-/// copies values from the targetDoc (which is the prototype of the pinDoc) to
-/// reserved fields on the pinDoc so that those values can be restored to the
-/// target doc when navigating to it.
+/**
+ * copies values from the targetDoc (which is the prototype of the pinDoc) to
+ * reserved fields on the pinDoc so that those values can be restored to the
+ * target doc when navigating to it.
+ * @param pinDoc Doc that will store pinned metadata
+ * @param pinProps description of props to pin
+ * @param targetDoc Doc that is being pinned
+ */
export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
const pinDoc = pinDocIn;
pinDoc.presentation = true;
@@ -60,7 +65,7 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
pinProps.pinData.scrollable ||
pinProps.pinData.temporal ||
pinProps.pinData.pannable ||
- pinProps.pinData.type_collection ||
+ pinProps.pinData.collectionType ||
pinProps.pinData.clippable ||
pinProps.pinData.datarange ||
pinProps.pinData.dataview ||
@@ -69,7 +74,7 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
const fkey = Doc.LayoutFieldKey(targetDoc);
if (pinProps.pinData.dataview) {
pinDoc.config_usePath = targetDoc[fkey + '_usePath'];
- pinDoc.config_data = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data;
+ pinDoc.config_data = Field.Copy(targetDoc[fkey]);
}
if (pinProps.pinData.dataannos) {
const fieldKey = Doc.LayoutFieldKey(targetDoc);
@@ -113,8 +118,8 @@ export function PinDocView(pinDocIn: Doc, pinProps: PinProps, targetDoc: Doc) {
})
)
);
- if (pinProps.pinData.type_collection) pinDoc.config_viewType = targetDoc._type_collection;
- if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField);
+ if (pinProps.pinData.collectionType) pinDoc.config_type_collection = targetDoc._type_collection;
+ if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField) ?? new List<string>();
if (pinProps.pinData.pivot) pinDoc.config_pivotField = targetDoc._pivotField;
if (pinProps.pinData.pannable) {
pinDoc.config_panX = NumCast(targetDoc._freeform_panX);
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 1f3ad8444..87076bf65 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -78,8 +78,8 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
_height: 50,
_layout_fitWidth: true,
_layout_autoHeight: true,
- _text_fontSize: StrCast(Doc.UserDoc().fontSize),
- _text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
+ text_fontSize: StrCast(Doc.UserDoc().fontSize),
+ text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
});
Doc.SetSelectOnLoad(target);
FormattedTextBox.DontSelectInitialText = true;
diff --git a/src/client/views/StyleProp.ts b/src/client/views/StyleProp.ts
index 44d3bf757..56367e70b 100644
--- a/src/client/views/StyleProp.ts
+++ b/src/client/views/StyleProp.ts
@@ -19,7 +19,9 @@ export enum StyleProp {
FontColor = 'fontColor', // color o tet
FontSize = 'fontSize', // size of text font
FontFamily = 'fontFamily', // font family of text
- FontWeight = 'fontWeight', // font weight of text
+ FontWeight = 'fontWeight', // font weight of text (eg bold)
+ FontStyle = 'fontStyle', // font style of text (eg italic)
+ FontDecoration = 'fontDecoration', // text decoration of text (eg underline)
Highlighting = 'highlighting', // border highlighting
ContextMenuItems = 'contextMenuItems', // menu items to add to context menu
AnchorMenuItems = 'anchorMenuItems',
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 02e0a34d8..3a5f57908 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -25,6 +25,7 @@ import { styleProviderQuiz } from './StyleProviderQuiz';
import { StyleProp } from './StyleProp';
import './StyleProvider.scss';
import { TagsView } from './TagsView';
+import { InkInkTool } from '../../fields/InkField';
function toggleLockedPosition(doc: Doc) {
UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -54,7 +55,6 @@ export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: nu
}
export function border(doc: Doc, pw: number, ph: number, rad: number = 0, inset: number = 0) {
- if (!rad) rad = 0;
const width = pw * inset;
const height = ph * inset;
@@ -172,7 +172,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(Doc.UserDoc().fontSize));
case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily));
case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(Doc.UserDoc().fontWeight));
- case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent')));
+ case StyleProp.FontStyle: return StrCast(doc?.[fieldKey + 'fontStyle'], StrCast(Doc.UserDoc().fontStyle));
+ case StyleProp.FontDecoration:return StrCast(doc?.[fieldKey + 'fontDecoration'], StrCast(Doc.UserDoc().fontDecoration));
+ case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, StrCast(Doc.UserDoc()[Doc.ActiveInk === InkInkTool.Highlight ? "inkHighlighterColor": "inkFillColor"], 'transparent'))));
case StyleProp.ShowCaption: return hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption);
case StyleProp.TitleHeight: return Math.min(4,(docView?.().screenToViewTransform().Scale ?? 1)) * NumCast(Doc.UserDoc().headerHeight,30);
case StyleProp.ShowTitle: return (
@@ -218,7 +220,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
const radiusRatio = borderRadius / docWidth;
const radius = radiusRatio * ((2 * borderWidth) + docWidth);
- const borderPath = doc && border(doc, NumCast(doc._width), NumCast(doc._height), radius, -ratio/2 ?? 0);
+ const borderPath = doc && border(doc, NumCast(doc._width), NumCast(doc._height), radius, -ratio/2);
return !borderPath
? null
: {
@@ -252,7 +254,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
case DocumentType.PRESELEMENT: docColor = docColor || ""; break;
case DocumentType.PRES: docColor = docColor || 'transparent'; break;
case DocumentType.FONTICON: docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY; break;
- case DocumentType.RTF: docColor = docColor || Colors.LIGHT_GRAY; break;
+ case DocumentType.RTF: docColor = docColor || StrCast(Doc.UserDoc().textBackgroundColor, Colors.LIGHT_GRAY); break;
case DocumentType.LINK: docColor = (isAnchor ? docColor : undefined); break;
case DocumentType.INK: docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; break;
case DocumentType.EQUATION: docColor = docColor || 'transparent'; break;
@@ -314,7 +316,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
? undefined // if it's a background & has a cluster color, make the shadow spread really big
: fieldKey.includes('_inline') // if doc is an inline document in a text box
? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0vw 0vw 0.1vw')}`
- : DocCast(doc.embedContainer)?.type === DocumentType.RTF && !isInk() // if doc is embedded in a text document (but not an inline)
+ :doc.rootDocument !== doc.embedContainer && DocCast(doc.embedContainer)?.type === DocumentType.RTF && !isInk() // if doc is embedded in a text document (but not an inline) and this isn't a simple text template (where the layoutDoc's rootDocument is its embed container)
? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}`
: StrCast(doc.layout_boxShadow, '');
}
diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx
index 8a1515d21..b3fb8c930 100644
--- a/src/client/views/StyleProviderQuiz.tsx
+++ b/src/client/views/StyleProviderQuiz.tsx
@@ -66,7 +66,7 @@ export namespace styleProviderQuiz {
newCol.zIndex = 1000;
newCol.forceActive = true;
newCol.quiz = text;
- newCol[DocData].textTransform = 'none';
+ newCol[DocData][Doc.LayoutFieldKey(newCol) + '_transform'] = 'none';
Doc.AddDocToList(img.Document, '_quizBoxes', newCol);
img.addDocument(newCol);
// img._loading = false;
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index 072cae3af..2615bc5fb 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -361,7 +361,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined,
transformOrigin: 'top left',
maxWidth: `${100 * this.currentScale}%`,
- width: 'max-content',
+ width: `${100 * this.currentScale}%`,
transform: `scale(${1 / this.currentScale})`,
backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index f66f6062e..b7980d74e 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -24,7 +24,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
promoteCollection?: () => void; // moves contents of collection to parent
updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
- restoreView?: (viewSpec: Doc) => boolean;
+ restoreView?: (viewSpec: Doc) => boolean; // DEPRECATED: do not use, it will go away. see PresBox.restoreTargetDocView
scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus
brushView?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number) => void; // highlight a region of a view (used by freeforms)
getView?: (doc: Doc, options: FocusViewOptions) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index 0637cd4e9..5283601bf 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -7,23 +7,31 @@
background-color: white;
overflow: hidden;
display: flex;
+ .collectionCardView-inner {
+ display: flex;
+ transform-origin: top left;
+ align-items: center;
+ }
button {
border-radius: 50%;
}
}
-.card-wrapper {
- display: grid;
- grid-template-columns: repeat(10, 1fr);
+.collectionCardView-flashcardUI {
+ top: 0;
+ position: absolute;
+ width: 100%;
+ height: 100%;
transform-origin: top left;
+}
- position: absolute;
+.collectionCardView-cardwrapper {
+ display: grid;
+ grid-template-columns: repeat(10, 1fr);
+ transform-origin: left 50%;
align-items: center;
- justify-items: center;
- justify-content: center;
-
- transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
+ z-index: 0; // so that setting z-index of active card doesn't make it land on top of things outside of the card-wrapper
}
.no-card-span {
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 286df30aa..0ba6b679c 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -4,8 +4,8 @@ import { computedFn } from 'mobx-utils';
import * as React from 'react';
import { ClientUtils, DashColor, imageUrlToBase64, returnFalse, returnNever, returnZero } from '../../../ClientUtils';
import { emptyFunction } from '../../../Utils';
-import { Doc } from '../../../fields/Doc';
-import { DocData } from '../../../fields/DocSymbols';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { Animation, DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { ScriptField } from '../../../fields/ScriptField';
@@ -13,14 +13,17 @@ import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } fr
import { URLField } from '../../../fields/URLField';
import { gptImageLabel } from '../../apis/gpt/GPT';
import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoable } from '../../util/UndoManager';
+import { PinDocView } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { TagItem } from '../TagsView';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
import './CollectionCardDeckView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
@@ -46,15 +49,15 @@ export class CollectionCardView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [key: string]: IReactionDisposer } = {};
private _textToDoc = new Map<string, Doc>();
+ private _oldWheel: HTMLElement | null = null;
private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center)
- private _clickScript = () => ScriptField.MakeScript('scriptContext._curDoc=this', { scriptContext: 'any' })!;
+ private _setCurDocScript = () => ScriptField.MakeScript('scriptContext.layoutDoc._card_curDoc=this', { scriptContext: 'any' })!;
@observable _forceChildXf = 0;
@observable _hoveredNodeIndex = -1;
@observable _docRefs = new ObservableMap<Doc, DocumentView>();
@observable _maxRowCount = 10;
@observable _docDraggedIndex: number = -1;
- @observable _curDoc: Doc | undefined = undefined;
constructor(props: SubCollectionViewProps) {
super(props);
@@ -66,6 +69,10 @@ export class CollectionCardView extends CollectionSubView() {
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
}
+ this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+ this._oldWheel = 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 });
};
/**
* Callback to ensure gpt's text versions of the child docs are updated
@@ -91,7 +98,7 @@ export class CollectionCardView extends CollectionSubView() {
if (isVis) {
this.openChatPopup();
} else {
- this.Document.cardSort = this.cardSort === cardSortings.Chat ? '' : this.Document.cardSort;
+ this.Document.card_sort = this.cardSort === cardSortings.Chat ? '' : this.Document.card_sort;
}
}
);
@@ -114,7 +121,25 @@ export class CollectionCardView extends CollectionSubView() {
}
@computed get cardSort() {
- return StrCast(this.Document.cardSort) as cardSortings;
+ return StrCast(this.Document.card_sort) as cardSortings;
+ }
+ /**
+ * Number of rows of cards to be rendered
+ */
+ @computed get numRows() {
+ return Math.ceil(this.sortedDocs.length / this._maxRowCount);
+ }
+ /**
+ * Circle arc size, in radians, to layout cards
+ */
+ @computed get archAngle() {
+ return NumCast(this.layoutDoc.card_arch, 90) * (Math.PI / 180) * (this.childCards.length < this._maxRowCount ? this.childCards.length / this._maxRowCount : 1);
+ }
+ /**
+ * Spacing card rows as a percent of Doc size. 100 means rows spread out to fill 100% of the Doc vertically. Default is 60%
+ */
+ @computed get cardSpacing() {
+ return NumCast(this.layoutDoc.card_spacing, 60);
}
/**
@@ -132,107 +157,57 @@ export class CollectionCardView extends CollectionSubView() {
return (this.childPanelWidth() * length) / this._props.PanelWidth();
}
+ @computed get nativeScaling() {
+ return this._props.NativeDimScaling?.() || 1;
+ }
+
/**
* When in quiz mode, randomly selects a document
*/
- quizMode = action(() => {
+ quizMode = () => {
const randomIndex = Math.floor(Math.random() * this.childDocs.length);
- this._curDoc = this.childDocs[randomIndex];
- });
-
- /**
- * Number of rows of cards to be rendered
- */
- @computed get numRows() {
- return Math.ceil(this.sortedDocs.length / this._maxRowCount);
- }
+ this.layoutDoc._card_curDoc = this.childDocs[randomIndex];
+ };
- @action
- setHoveredNodeIndex = (index: number) => {
+ setHoveredNodeIndex = action((index: number) => {
if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index;
- };
+ });
isSelected = (doc: Doc) => this._docRefs.get(doc)?.IsSelected;
- childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2);
+ childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childCards.length > this._maxRowCount ? this._maxRowCount : this.childCards.length) / this.nativeScaling));
childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale;
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this.isAnyChildContentActive();
isAnyChildContentActive = this._props.isAnyChildContentActive;
/**
- * Returns the degree to rotate a card dependind on the amount of cards in their row and their index in said row
- * @param amCards
- * @param index
- * @returns
- */
- rotate = (amCards: number, index: number) => {
- if (amCards == 1) return 0;
-
- const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2));
- if (amCards % 2 === 0) {
- if (possRotate === 0) {
- return possRotate + Math.abs(-30 + (index - 1) * (30 / (amCards / 2)));
- }
- if (index > (amCards + 1) / 2) {
- const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2)));
- return possRotate + stepMag;
- }
- }
-
- return possRotate;
- };
- /**
- * Returns the degree to which a card should be translated in the y direction for the arch effect
- */
- translateY = (amCards: number, index: number, realIndex: number) => {
- const evenOdd = amCards % 2;
- const apex = (amCards - evenOdd) / 2;
- const Magnitude = this.childPanelWidth() / 2; // 400
- const stepMag = Magnitude / 2 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25);
-
- let rowOffset = 0;
- if (realIndex > this._maxRowCount - 1) {
- rowOffset = Magnitude * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount);
- }
- if (evenOdd === 1 || index < apex - 1) {
- return Math.abs(stepMag * (apex - index)) - rowOffset;
- }
- if (index === apex || index === apex - 1) {
- return 0 - rowOffset;
- }
-
- return Math.abs(stepMag * (apex - index - 1)) - rowOffset;
- };
-
- /**
* When dragging a card, determines the index the card should be set to if dropped
* @param mouseX mouse's x location
* @param mouseY mouses' y location
* @returns the card's new index
*/
findCardDropIndex = (mouseX: number, mouseY: number) => {
- const amCardsTotal = this.sortedDocs.length;
+ const cardCount = this.sortedDocs.length;
let index = 0;
- const cardWidth = amCardsTotal < this._maxRowCount ? this._props.PanelWidth() / amCardsTotal : this._props.PanelWidth() / this._maxRowCount;
+ const cardWidth = cardCount < this._maxRowCount ? this._props.PanelWidth() / cardCount : this._props.PanelWidth() / this._maxRowCount;
// Calculate the adjusted X position accounting for the initial offset
let adjustedX = mouseX;
- const amRows = Math.ceil(amCardsTotal / this._maxRowCount);
- const rowHeight = this._props.PanelHeight() / amRows;
+ const rowHeight = this._props.PanelHeight() / this.numRows;
const currRow = Math.floor((mouseY - 100) / rowHeight); //rows start at 0
if (adjustedX < 0) {
return 0; // Before the first column
}
- if (amCardsTotal < this._maxRowCount) {
+ if (cardCount < this._maxRowCount) {
index = Math.floor(adjustedX / cardWidth);
- } else if (currRow != amRows - 1) {
+ } else if (currRow != this.numRows - 1) {
index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
} else {
- const rowAmCards = amCardsTotal - currRow * this._maxRowCount;
- const offset = ((this._maxRowCount - rowAmCards) / 2) * cardWidth;
+ const cardsInRow = cardCount - currRow * this._maxRowCount;
+ const offset = ((this._maxRowCount - cardsInRow) / 2) * cardWidth;
adjustedX = mouseX - offset;
index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
@@ -241,11 +216,14 @@ export class CollectionCardView extends CollectionSubView() {
};
/**
- * Checks to see if a card is being dragged and calls the appropriate methods if so
+ * if pointer moves over cardDeck while dragging a Doc that is in the Deck or that can be dropped in the deck,
+ * then this sets the card index where the dragged card would be added.
*/
@action
onPointerMove = (x: number, y: number) => {
- this._docDraggedIndex = DragManager.docsBeingDragged.length ? this.findCardDropIndex(x, y) : -1;
+ if (DragManager.docsBeingDragged.some(doc => this.sortedDocs.includes(doc)) || SnappingManager.CanEmbed) {
+ this._docDraggedIndex = this.findCardDropIndex(x, y);
+ }
};
/**
@@ -264,7 +242,7 @@ export class CollectionCardView extends CollectionSubView() {
const sorted = this.sortedDocs;
const originalIndex = sorted.findIndex(doc => doc === draggedDoc);
- this.Document.cardSort = '';
+ this.Document.card_sort = '';
originalIndex !== -1 && sorted.splice(originalIndex, 1);
sorted.splice(dragIndex, 0, draggedDoc);
if (de.complete.docDragData.removeDocument?.(draggedDoc)) {
@@ -280,15 +258,6 @@ export class CollectionCardView extends CollectionSubView() {
''
);
- @computed get sortedDocs() {
- return this.sort(
- this.childCards.map(card => card.layout),
- this.cardSort,
- BoolCast(this.Document.cardSort_isDesc),
- this._docDraggedIndex
- );
- }
-
/**
* Used to determine how to sort cards based on tags. The leftmost tags are given lower values while cards to the right are
* given higher values. Decimals are used to determine placement for cards with multiple tags
@@ -336,11 +305,20 @@ export class CollectionCardView extends CollectionSubView() {
return docs;
};
+ @computed get sortedDocs() {
+ return this.sort(
+ this.childCards.map(card => card.layout),
+ this.cardSort,
+ BoolCast(this.Document.card_sort_isDesc),
+ this._docDraggedIndex
+ );
+ }
+
isChildContentActive = computedFn(
(doc: Doc) => () =>
this._props.isContentActive?.() === false
? false
- : this._props.isDocumentActive?.() && this._curDoc === doc
+ : this._props.isDocumentActive?.() && this.curDoc() === doc
? true
: this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
? false
@@ -354,74 +332,100 @@ export class CollectionCardView extends CollectionSubView() {
Document={doc}
NativeWidth={returnZero}
NativeHeight={returnZero}
- fitWidth={returnFalse}
- onDoubleClickScript={this.onChildDoubleClick}
+ PanelWidth={this.childPanelWidth}
+ PanelHeight={this.childPanelHeight}
renderDepth={this._props.renderDepth + 1}
LayoutTemplate={this._props.childLayoutTemplate}
LayoutTemplateString={this._props.childLayoutString}
containerViewPath={this.childContainerViewPath}
ScreenToLocalTransform={screenToLocalTransform} // makes sure the box wrapper thing is in the right spot
isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
- PanelWidth={this.childPanelWidth}
- PanelHeight={this.childPanelHeight}
+ isContentActive={this.isChildContentActive(doc)}
+ fitWidth={returnFalse}
waitForDoubleClickToClick={returnNever}
scriptContext={this}
- onClickScript={this._curDoc === doc ? undefined : this._clickScript}
+ focus={this.focus}
+ onDoubleClickScript={this.onChildDoubleClick}
+ onClickScript={this.curDoc() === doc ? undefined : this._setCurDocScript}
dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice.
dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
showTags={BoolCast(this.layoutDoc.showChildTags)}
whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
- isContentActive={this.isChildContentActive(doc)}
dontHideOnDrag
/>
);
/**
* Determines how many cards are in the row of a card at a specific index
- * @param index
- * @returns
+ * @param index numerical index of card in total list of all cards
+ * @returns number of cards in row that contains index
*/
- overflowAmCardsCalc = (index: number) => {
- if (this.sortedDocs.length < this._maxRowCount) {
- return this.sortedDocs.length;
+ cardsInRowThatIncludesCardIndex = (index: number) => {
+ if (this.childCards.length < this._maxRowCount) {
+ return this.childCards.length;
}
- const totalCards = this.sortedDocs.length;
- // if 9 or less
+ const totalCards = this.childCards.length;
if (index < totalCards - (totalCards % this._maxRowCount)) {
return this._maxRowCount;
}
return totalCards % this._maxRowCount;
};
/**
- * Determines the index a card is in in a row
- * @param realIndex
- * @returns
+ * Determines the index a card is in in a row. If the row is not full, then the cards
+ * are centered within the row (as if unrendered cards had been added to the start and end
+ * of the row) and the retuned index is the index the card in this virtual full row.
+ * @param index numerical index of card in total list of all cards
+ * @returns index of card in its row, normalized to a full size row
*/
- overflowIndexCalc = (realIndex: number) => realIndex % this._maxRowCount;
+ centeredIndexOfCardInRow = (index: number) => {
+ const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
+ const lineIndex = index % this._maxRowCount;
+ if (cardsInRow === this._maxRowCount) return lineIndex;
+ return lineIndex + (this._maxRowCount - cardsInRow) / 2;
+ };
/**
- * Translates the cards in the second rows and beyond over to the right
- * @param realIndex
- * @param calcIndex
- * @param calcRowCards
- * @returns
+ * Returns the rotation of a card in radians based on its horizontal location (and thus m apping to a circle arc).
+ * The amount of rotation is goverend by the Doc's card_arch field which specifies, in degrees, the range of a circle
+ * arc that cards should cover -- by default, -45 to 45 degrees.
+ * @param index numerical index of card in total list of all cards
+ * @returns angle of rotation in radians
+ */
+ rotate = (index: number) => {
+ const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
+ const centeredIndexInRow = (cardsInRow < this._maxRowCount ? index + (this._maxRowCount - cardsInRow) / 2 : index) % this._maxRowCount;
+ const rowIndexMax = this._maxRowCount - 1;
+ return ((this.archAngle / 2) * (centeredIndexInRow - rowIndexMax / 2)) / (rowIndexMax / 2);
+ };
+ /**
+ * Provides a vertical adjustment to a card's grid position so that it will lie along an arch.
+ * @param index numerical index of card in total list of all cards
+ */
+ translateY = (index: number) => {
+ const Magnitude = ((this._props.PanelHeight() * this.fitContentScale) / 2) * Math.sqrt(((this.archAngle * (180 / Math.PI)) / 60) * 4);
+ return Magnitude * (1 - Math.sin(this.rotate(index) + Math.PI / 2) - (1 - Math.sin(this.archAngle / 2 + Math.PI / 2)) / 2);
+ };
+ /**
+ * When the card index is for a row (not the first row) that is not full, this returns a horizontal adjustment that centers the row
+ * @param index index of card from start of deck
+ * @param cardsInRow number of cards in the row containing the indexed card
+ * @returns horizontal pixel translation
*/
- translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (this._maxRowCount - calcRowCards) * (this.childPanelWidth() / 2));
+ horizontalAdjustmentForPartialRows = (index: number, cardsInRow: number) => (index < this._maxRowCount ? 0 : (this._maxRowCount - cardsInRow) * (this.childPanelWidth() / 2));
/**
- * Determines how far to translate a card in the y direction depending on its index and if it's selected
+ * Adjusts the vertical placement of the card from its grid position so that it will either line on a
+ * circular arc if the card isn't active, or so that it will be centered otherwise.
* @param isActive whether the card is focused for interaction
- * @param realIndex index of card from start of deck
- * @param amCards ??
- * @param calcRowIndex index of card from start of row
- * @returns Y translation of card
+ * @param index index of card from start of deck
+ * @returns vertical pixel translation
*/
- calculateTranslateY = (isActive: boolean, realIndex: number, amCards: number, calcRowIndex: number) => {
- const rowHeight = (this._props.PanelHeight() * this.fitContentScale) / this.numRows;
- const rowIndex = Math.trunc(realIndex / this._maxRowCount);
+ adjustCardYtoFitArch = (isActive: boolean, index: number) => {
+ const rowHeight = this._props.PanelHeight() / this.numRows;
+ const rowIndex = Math.floor(index / this._maxRowCount);
const rowToCenterShift = this.numRows / 2 - rowIndex;
- if (isActive) return rowToCenterShift * rowHeight - rowHeight / 2;
- if (amCards == 1) return 50 * this.fitContentScale;
- return this.translateY(amCards, calcRowIndex, realIndex);
+ return isActive
+ ? (rowToCenterShift * rowHeight - rowHeight / 2) * ((this.cardSpacing * this.fitContentScale) / 100) //
+ : this.translateY(index);
};
/**
@@ -488,7 +492,7 @@ export class CollectionCardView extends CollectionSubView() {
}
if (questionType === '6') {
- this.Document.cardSort = 'chat';
+ this.Document.card_sort = 'chat';
}
listItems.forEach((item, index) => {
@@ -544,7 +548,7 @@ export class CollectionCardView extends CollectionSubView() {
await this.childPairStringListAndUpdateSortDesc();
};
- childScreenToLocal = computedFn((doc: Doc, index: number, calcRowIndex: number, isSelected: boolean, amCards: number) => () => {
+ childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
// need to explicitly trigger an invalidation since we're reading everything from the Dom
this._forceChildXf;
this._props.ScreenToLocalTransform();
@@ -555,64 +559,91 @@ export class CollectionCardView extends CollectionSubView() {
return new Transform(-translateX + (dref?.centeringX || 0) * scale,
-translateY + (dref?.centeringY || 0) * scale, 1)
- .scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore
+ .scale(1 / scale).rotate(!isSelected ? -this.rotate(this.centeredIndexOfCardInRow(index)) : 0); // prettier-ignore
+ });
+
+ /**
+ * Releases the currently focused Doc by deselecting it and returning it to its location on the arch, and selecting the
+ * cardDeck itself.
+ * This will also force the Doc to recompute its layout transform when the animation completes.
+ * In addition, this sets an animating flag on the Doc so that it will receive no poiner events when animating, such as hover
+ * events that would trigger a flashcard to flip.
+ * @param doc doc that will be animated away from center focus
+ */
+ releaseCurDoc = action(() => {
+ const selDoc = this.curDoc();
+ this.layoutDoc._card_curDoc = undefined;
+ const cardDocView = DocumentView.getDocumentView(selDoc, this.DocumentView?.());
+ if (cardDocView && selDoc) {
+ DocumentView.DeselectView(cardDocView);
+ this._props.select(false);
+ selDoc[Animation] = selDoc; // turns off pointer events & doc decorations while animating - useful for flashcards that reveal back on hover
+ setTimeout(action(() => {
+ selDoc[Animation] = undefined;
+ this._forceChildXf++;
+ }), 350); // prettier-ignore
+ }
});
+ /**
+ * turns off the _dropped flag at the end of a drag/drop, or releases the focused Doc if a different Doc is clicked
+ */
cardPointerUp = action((doc: Doc) => {
- // if a card doc has just moved, or a card is selected and in front, then ignore this event
- if (this._curDoc === doc || this._dropped) {
+ if (this.curDoc() === doc || this._dropped) {
this._dropped = false;
} else {
- // otherwise, turn off documentDecorations becase we're in a selection transition and want to avoid artifacts.
- // Turn them back on when the animation has completed and the render and backend structures are in synch
- SnappingManager.SetHideDecorations(true);
- setTimeout(
- action(() => {
- SnappingManager.SetHideDecorations(false);
- this._forceChildXf++;
- }),
- 1000
+ this.releaseCurDoc(); // NOTE: the onClick script for the card will select the new card (ie, 'doc')
+ }
+ });
+
+ focus = action((anchor: Doc, options: FocusViewOptions): Opt<number> => {
+ const docs = DocListCast(this.Document[this.fieldKey]);
+ if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+ const foundDoc = DocCast(
+ anchor.config_card_curDoc,
+ docs.find(doc => doc === DocCast(anchor.annotationOn, anchor))
);
+ options.didMove = foundDoc !== this.curDoc() ? true : false;
+ options.didMove && (this.layoutDoc._card_curDoc = foundDoc);
}
+ return undefined;
});
+ getAnchor = (addAsAnnotation: boolean) => {
+ const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_card_curDoc: this.curDoc() });
+ PinDocView(anchor, { pinData: { collectionType: true, filters: true } }, this.Document);
+ addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+ return anchor;
+ };
+ addDocTab = this.addLinkedDocTab;
/**
* Actually renders all the cards
*/
@computed get renderCards() {
- if (!this.childCards.length) {
- return null;
- }
-
// Map sorted documents to their rendered components
return this.sortedDocs.map((doc, index) => {
- const calcRowIndex = this.overflowIndexCalc(index);
- const amCards = this.overflowAmCardsCalc(index);
+ const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
- const childScreenToLocal = this.childScreenToLocal(doc, index, calcRowIndex, doc === this._curDoc, amCards);
+ const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this.curDoc());
+
+ const translateToCenterIfActive = () => (doc === this.curDoc() ? (cardsInRow / 2 - (index % this._maxRowCount)) * 100 - 50 : 0);
- const translateIfSelected = () => {
- const indexInRow = index % this._maxRowCount;
- const rowIndex = Math.trunc(index / this._maxRowCount);
- const rowCenterIndex = Math.min(this._maxRowCount, this.sortedDocs.length - rowIndex * this._maxRowCount) / 2;
- return (rowCenterIndex - indexInRow) * 100 - 50;
- };
const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
- const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()),
+ const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale * this.nativeScaling) / (aspect * this.childPanelWidth()),
(this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore
const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
return (
<div
key={doc[Id]}
- className={`card-item${doc === this._curDoc ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`}
+ className={`card-item${doc === this.curDoc() ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`}
onPointerUp={() => this.cardPointerUp(doc)}
style={{
width: this.childPanelWidth(),
height: 'max-content',
- transform: `translateY(${this.calculateTranslateY(doc === this._curDoc, index, amCards, calcRowIndex)}px)
- translateX(calc(${doc === this._curDoc ? translateIfSelected() : 0}% + ${this.translateOverflowX(index, amCards)}px))
- rotate(${doc !== this._curDoc ? this.rotate(amCards, calcRowIndex) : 0}deg)
- scale(${doc === this._curDoc ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`,
+ transform: `translateY(${this.adjustCardYtoFitArch(doc === this.curDoc(), index)}px)
+ translateX(calc(${translateToCenterIfActive()}% + ${this.horizontalAdjustmentForPartialRows(index, cardsInRow)}px))
+ rotate(${doc !== this.curDoc()? this.rotate(index) : 0}rad)
+ scale(${doc === this.curDoc()? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`,
}} // prettier-ignore
onPointerEnter={() => this.setHoveredNodeIndex(index)}
onPointerLeave={() => this.setHoveredNodeIndex(-1)}>
@@ -621,6 +652,7 @@ export class CollectionCardView extends CollectionSubView() {
);
});
}
+ onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
docViewProps = (): DocumentViewProps => ({
@@ -629,27 +661,20 @@ export class CollectionCardView extends CollectionSubView() {
isContentActive: emptyFunction,
ScreenToLocalTransform: this.contentScreenToLocalXf,
});
- answered = action(() => {
- this._curDoc = this.curDoc ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(this.curDoc) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
- });
- curDoc = () => this._curDoc;
+ answered = () => {
+ this.layoutDoc._card_curDoc = this.curDoc() ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(d => d === this.curDoc()) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
+ };
+ curDoc = () => DocCast(this.layoutDoc._card_curDoc);
render() {
- const isEmpty = this.childCards.length === 0;
+ const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
return (
<div
className="collectionCardView-outer"
ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
- onPointerDown={action(() => {
- this._curDoc = undefined;
- SnappingManager.SetHideDecorations(true);
- setTimeout(
- action(() => {
- SnappingManager.SetHideDecorations(false);
- this._forceChildXf++;
- }),
- 1000
- );
+ onPointerDown={action(e => {
+ if (e.button === 2 || e.ctrlKey) return;
+ this.releaseCurDoc();
})}
onPointerLeave={action(() => (this._docDraggedIndex = -1))}
onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))}
@@ -659,16 +684,31 @@ export class CollectionCardView extends CollectionSubView() {
color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
}}>
<div
- className="card-wrapper"
+ className="collectionCardView-inner"
style={{
- ...(!isEmpty && { transform: `scale(${1 / this.fitContentScale})` }),
- ...{ height: `${100 * (isEmpty ? 1 : this.fitContentScale)}%` },
- ...{ width: `${100 * (isEmpty ? 1 : this.fitContentScale)}%` },
- gridAutoRows: `${100 / this.numRows}%`,
+ transform: `scale(${1 / fitContentScale})`,
+ height: `${100 * fitContentScale}%`,
+ width: `${100 * fitContentScale}%`,
}}>
- {this.renderCards}
+ <div
+ className="collectionCardView-cardwrapper"
+ style={{
+ gridAutoRows: `${100 / this.numRows}%`,
+ height: `${this.cardSpacing}%`,
+ }}>
+ {this.renderCards}
+ </div>
+ <div
+ className="collectionCardView-flashcardUI"
+ style={{
+ pointerEvents: this.childCards.length === 0 ? undefined : 'none',
+ height: `${100 / this.nativeScaling / fitContentScale}%`,
+ width: `${100 / this.nativeScaling / fitContentScale}%`,
+ transform: `scale(${this.nativeScaling * fitContentScale})`,
+ }}>
+ {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
+ </div>
</div>
- {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
</div>
);
}
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index f2ba90c78..c080ba27e 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -1,6 +1,7 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
+import { computedFn } from 'mobx-utils';
import * as React from 'react';
import { returnZero } from '../../../ClientUtils';
import { Utils } from '../../../Utils';
@@ -8,8 +9,10 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { Transform } from '../../util/Transform';
+import { PinDocView } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { DocumentView } from '../nodes/DocumentView';
import { FocusViewOptions } from '../nodes/FocusViewOptions';
@@ -22,12 +25,16 @@ const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = requi
@observer
export class CollectionCarousel3DView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
+ private _oldWheel: HTMLElement | null = null;
constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
}
+ componentDidMount(): void {
+ this._props.setContentViewBox?.(this);
+ }
componentWillUnmount() {
this._dropDisposer?.();
}
@@ -37,6 +44,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
}
+ this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+ this._oldWheel = 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 });
};
@computed get scrollSpeed() {
@@ -48,45 +59,61 @@ export class CollectionCarousel3DView extends CollectionSubView() {
centerScale = Number(CAROUSEL3D_CENTER_SCALE);
sideScale = Number(CAROUSEL3D_SIDE_SCALE);
- panelWidth = () => this._props.PanelWidth() / 3;
- panelHeight = () => this._props.PanelHeight() * this.sideScale;
+ panelWidth = () => this._props.PanelWidth() / 3 / this.nativeScaling();
+ panelHeight = () => (this._props.PanelHeight() * this.sideScale) / this.nativeScaling();
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
- isChildContentActive = () =>
- this._props.isContentActive?.() === false
- ? false
- : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
- ? true
- : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
+ isChildContentActive = computedFn(
+ (doc: Doc) => () =>
+ this._props.isContentActive?.() === false
? false
- : undefined;
- contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
+ : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
+ ? true
+ : this._props.isContentActive?.() && this.curDoc() === doc
+ ? true
+ : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
+ ? false
+ : undefined
+ );
+ contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().translate(0, (-(Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) / this.nativeScaling());
childScreenLeftToLocal = () =>
this.contentScreenToLocalXf()
- .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+ .translate(
+ (-this.panelWidth() * (1 - this.sideScale)) / 2, //
+ (-this.panelHeight() * (1 - this.sideScale)) / 2
+ )
.scale(1 / this.sideScale);
childScreenRightToLocal = () =>
this.contentScreenToLocalXf()
- .translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+ .translate(
+ -2 * this.panelWidth() - (this.panelWidth() * (1 - this.sideScale)) / 2, //
+ (-this.panelHeight() * (1 - this.sideScale)) / 2
+ )
.scale(1 / this.sideScale);
childCenterScreenToLocal = () =>
this.contentScreenToLocalXf()
.translate(
-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3
- -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2
- ) // top is top margin % of panelHeight - increased size percent of center * panelHeight / 2
+ ((this.centerScale - 1) * this.panelHeight()) / 2
+ )
.scale(1 / this.centerScale);
focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
- const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]);
- if (anchor.type !== DocumentType.CONFIG && !docs.includes(anchor)) return undefined;
- options.didMove = true;
- const target = DocCast(anchor.annotationOn) ?? anchor;
- const index = docs.indexOf(target);
- index !== -1 && (this.layoutDoc._carousel_index = index);
+ const docs = DocListCast(this.Document[this.fieldKey]);
+ if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+ const newIndex = anchor.config_carousel_index ?? docs.getIndex(DocCast(anchor.annotationOn, anchor));
+ options.didMove = newIndex !== this.layoutDoc._carousel_index;
+ options.didMove && (this.layoutDoc._carousel_index = newIndex);
+ }
return undefined;
};
-
+ getAnchor = (addAsAnnotation: boolean) => {
+ const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.layoutDoc._carousel_index as number });
+ PinDocView(anchor, { pinData: { collectionType: true, filters: true } }, this.Document);
+ addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+ return anchor;
+ };
+ addDocTab = this.addLinkedDocTab;
@computed get content() {
const currentIndex = NumCast(this.layoutDoc._carousel_index);
const displayDoc = (child: Doc, dxf: () => Transform) => (
@@ -105,7 +132,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
LayoutTemplateString={this._props.childLayoutString}
focus={this.focus}
ScreenToLocalTransform={dxf}
- isContentActive={this.isChildContentActive}
+ isContentActive={this.isChildContentActive(child)}
isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
@@ -120,7 +147,6 @@ export class CollectionCarousel3DView extends CollectionSubView() {
}
changeSlide = (direction: number) => {
- DocumentView.DeselectAll();
this.layoutDoc._carousel_index = !this.curDoc() ? 0 : (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % (this.carouselItems.length || 1);
};
@@ -194,14 +220,16 @@ export class CollectionCarousel3DView extends CollectionSubView() {
return this.panelWidth() * (1 - index);
}
+ onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
curDoc = () => this.carouselItems[NumCast(this.layoutDoc._carousel_index)]?.layout;
- answered = (correct: boolean) => (!correct || !this.curDoc()) && this.changeSlide(1);
+ answered = (correct: boolean) => (!correct || !this.curDoc() || NumCast(this.layoutDoc._carousel_index) === this.carouselItems.length - 1) && this.changeSlide(1);
docViewProps = () => ({
...this._props, //
isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
- isContentActive: this.isChildContentActive,
+ isContentActive: this._props.isContentActive,
ScreenToLocalTransform: this.contentScreenToLocalXf,
});
+ nativeScaling = () => this._props.NativeDimScaling?.() || 1;
render() {
return (
<div
@@ -210,6 +238,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
style={{
background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
+ transformOrigin: 'top left',
+ transform: `scale(${this.nativeScaling()})`,
+ width: `${100 / this.nativeScaling()}%`,
+ height: `${100 / this.nativeScaling()}%`,
}}>
<div className="carousel-wrapper" style={{ transform: `translateX(${this.translateX}px)` }}>
{this.content}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index aa447c7bf..f714e2a00 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -3,12 +3,16 @@ import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
+import { PinDocView } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import './CollectionCarouselView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
@@ -17,6 +21,7 @@ import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
export class CollectionCarouselView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
+ _oldWheel: HTMLElement | null = null;
_fadeTimer: NodeJS.Timeout | undefined;
@observable _last_index = this.carouselIndex;
@observable _last_opacity = 1;
@@ -26,6 +31,9 @@ export class CollectionCarouselView extends CollectionSubView() {
makeObservable(this);
}
+ componentDidMount(): void {
+ this._props.setContentViewBox?.(this);
+ }
componentWillUnmount() {
this._dropDisposer?.();
}
@@ -35,6 +43,10 @@ export class CollectionCarouselView extends CollectionSubView() {
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
}
+ this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+ this._oldWheel = 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 });
};
@computed get captionMarginX(){ return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
@@ -53,44 +65,56 @@ export class CollectionCarouselView extends CollectionSubView() {
/**
* Goes to the next Doc in the stack subject to the currently selected filter option.
*/
- advance = (e?: React.MouseEvent) => {
- e?.stopPropagation();
- this.move(1);
- };
+ advance = () => this.move(1);
/**
* Goes to the previous Doc in the stack subject to the currently selected filter option.
*/
- goback = (e: React.MouseEvent) => {
- e.stopPropagation();
- this.move(-1);
- };
+ goback = () => this.move(-1);
curDoc = () => this.carouselItems[this.carouselIndex]?.layout;
+ focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
+ const docs = DocListCast(this.Document[this.fieldKey]);
+ if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+ const newIndex = anchor.config_carousel_index ?? docs.getIndex(DocCast(anchor.annotationOn, anchor));
+ options.didMove = newIndex !== this.layoutDoc._carousel_index;
+ options.didMove && (this.layoutDoc._carousel_index = newIndex);
+ }
+ return undefined;
+ };
+
+ getAnchor = (addAsAnnotation: boolean) => {
+ const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.carouselIndex });
+ PinDocView(anchor, { pinData: { collectionType: true, filters: true } }, this.Document);
+ addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+ return anchor;
+ };
+ addDocTab = this.addLinkedDocTab;
+
captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
// first look for properties on the document in the carousel, then fallback to properties on the container
const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property);
};
- contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
- contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin);
+ contentPanelWidth = () => (this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin)) / this.nativeScaling();
+ contentPanelHeight = () => (this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin)) / this.nativeScaling();
onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX;
contentScreenToLocalXf = () =>
this._props
- .ScreenToLocalTransform()
- .translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin))
- .scale(this._props.NativeDimScaling?.() || 1);
+ .ScreenToLocalTransform() //
+ .translate(-NumCast(this.layoutDoc.xMargin) / this.nativeScaling(), -NumCast(this.layoutDoc.yMargin) / this.nativeScaling());
isChildContentActive = () =>
this._props.isContentActive?.() === false
? false
- : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
+ : this._props.isContentActive()
? true
: this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
? false
- : undefined;
+ : undefined; // prettier-ignore
+ onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
return (
<DocumentView
@@ -114,6 +138,7 @@ export class CollectionCarouselView extends CollectionSubView() {
LayoutTemplateString={this._props.childLayoutString}
TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)}
childFilters={this.childDocFilters}
+ focus={this.focus}
hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
addDocument={this._props.addDocument}
ScreenToLocalTransform={this.contentScreenToLocalXf}
@@ -196,30 +221,37 @@ export class CollectionCarouselView extends CollectionSubView() {
);
}
+ nativeScaling = () => this._props.NativeDimScaling?.() || 1;
+
docViewProps = () => ({
...this._props, //
isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
isContentActive: this.isChildContentActive,
ScreenToLocalTransform: this.contentScreenToLocalXf,
});
- answered = () => this.advance();
+ answered = (correct: boolean) => (!correct || !this.curDoc() || NumCast(this.layoutDoc._carousel_index) === this.carouselItems.length - 1) && this.advance();
render() {
return (
- <div
- className="collectionCarouselView-outer"
- ref={this.createDashEventsTarget}
- style={{
- background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
- color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
- width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`,
- height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`,
- left: NumCast(this.layoutDoc._xMargin),
- top: NumCast(this.layoutDoc._yMargin),
- }}>
- {this.content}
- {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
- {this.navButtons}
+ <div>
+ <div
+ className="collectionCarouselView-outer"
+ ref={this.createDashEventsTarget}
+ style={{
+ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
+ color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
+ left: NumCast(this.layoutDoc._xMargin),
+ top: NumCast(this.layoutDoc._yMargin),
+ transformOrigin: 'top left',
+ transform: `scale(${this.nativeScaling()})`,
+ width: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._xMargin)) / this.nativeScaling()}px)`,
+ height: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._yMargin)) / this.nativeScaling()}px)`,
+ position: 'relative',
+ }}>
+ {this.content}
+ {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
+ {this.navButtons}
+ </div>
</div>
);
}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e1786d2c9..3e8138390 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -453,7 +453,7 @@ export class CollectionDockingView extends CollectionSubView() {
}
}
}
- if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && Doc.ActiveTool !== InkTool.Ink) {
e.stopPropagation();
}
};
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 486c826b6..c3047e5fb 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable no-use-before-define */
import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -59,7 +58,6 @@ export enum TrimScope {
@observer
export class CollectionStackedTimeline extends CollectionSubView<CollectionStackedTimelineProps>() {
- // eslint-disable-next-line no-use-before-define
public static SelectingRegions: Set<CollectionStackedTimeline> = new Set();
public static StopSelecting() {
this.SelectingRegions.forEach(
@@ -171,7 +169,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
makeDocUnfiltered = (doc: Doc) => this.childDocList?.some(item => item === doc);
- getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
+ getView = (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
new Promise<Opt<DocumentView>>(res => {
if (doc.hidden) options.didMove = !(doc.hidden = false);
const findDoc = (finish: (dv: DocumentView) => void) => DocumentView.addViewRenderedCb(doc, dv => finish(dv));
@@ -600,7 +598,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
pointerEvents: 'none',
}}>
<StackedTimelineAnchor
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
mark={d.anchor}
containerViewPath={this._props.containerViewPath}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index f85b0b433..0c059f729 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -27,6 +27,7 @@ import { ViewBoxBaseComponent } from '../DocComponent';
import { FieldViewProps } from '../nodes/FieldView';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import { FlashcardPracticeUI } from './FlashcardPracticeUI';
+import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
@@ -130,6 +131,17 @@ export function CollectionSubView<X>() {
@computed get childDocList() {
return Cast(this.dataField, listSpec(Doc));
}
+
+ addLinkedDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
+ const doc = toList(docsIn).lastElement();
+ const where = location.split(':')[0];
+ if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) {
+ if (doc.hidden) doc.hidden = false;
+ if (!location.includes(OpenWhereMod.always)) return true;
+ }
+ return this._props.addDocTab(docsIn, location);
+ };
+
collectionFilters = () => this._focusFilters ?? StrListCast(this.Document._childFilters);
collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.Document._childFiltersByRanges, listSpec('string'), []);
// child filters apply to the descendants of the documents in this collection
@@ -519,17 +531,18 @@ export function CollectionSubView<X>() {
};
protected _sideBtnWidth = 35;
+ protected _sideBtnMaxPanelPct = 0.15;
@observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
/**
* How much the content of the collection is being scaled based on its nesting and its fit-to-width settings
*/
- @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
+ @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore
/**
* The maximum size a UI widget can be in collection coordinates based on not wanting the widget to visually obscure too much of the collection
* This takes the desired screen space size and converts into collection coordinates. It then returns the smaller of the converted
* size or a fraction of the collection view.
*/
- @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, 0.25 * NumCast(this.layoutDoc.width, 1)); } // prettier-ignore
+ @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width)? 1: this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1)); } // prettier-ignore
/**
* This computes a scale factor for UI elements so that they shrink and grow as the collection does in screen space.
* Note, the scale factor does not allow for elements to grow larger than their native screen space size.
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 8a24db330..d75c633ac 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -52,7 +52,7 @@ export class CollectionTimeView extends CollectionSubView() {
title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as unknown as string, // title can take a functiono or a string
annotationOn: this.Document,
});
- PinDocView(anchor, { pinData: { type_collection: true, pivot: true, filters: true } }, this.Document);
+ PinDocView(anchor, { pinData: { collectionType: true, pivot: true, filters: true } }, this.Document);
if (addAsAnnotation) {
// when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
@@ -147,7 +147,6 @@ export class CollectionTimeView extends CollectionSubView() {
return (
<div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
<CollectionFreeFormView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }}
fitContentsToBox={returnTrue}
@@ -257,7 +256,6 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou
const pivotView = DocumentView.getDocumentView(pivotDoc);
if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
- // eslint-disable-next-line prefer-destructuring
pivotDoc._pivotField = filterVals[0];
}
}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7418d4360..6f0833a22 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -83,7 +83,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
return viewField;
}
- screenToLocalTransform = () => (this._props.renderDepth ? this.ScreenToLocalBoxXf() : this.ScreenToLocalBoxXf().scale(this._props.PanelWidth() / this.bodyPanelWidth()));
+ screenToLocalTransform = this.ScreenToLocalBoxXf;
// prettier-ignore
private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => {
TraceMobx();
@@ -202,8 +202,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
}
};
- bodyPanelWidth = () => this._props.PanelWidth();
-
childLayoutTemplate = () => this._props.childLayoutTemplate?.() || Cast(this.Document.childLayoutTemplate, Doc, null);
isContentActive = () => this._isContentActive;
@@ -221,7 +219,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
removeDocument: this.removeDocument,
isContentActive: this.isContentActive,
isAnyChildContentActive: this.isAnyChildContentActive,
- PanelWidth: this.bodyPanelWidth,
+ PanelWidth: this._props.PanelWidth,
PanelHeight: this._props.PanelHeight,
ScreenToLocalTransform: this.screenToLocalTransform,
childLayoutTemplate: this.childLayoutTemplate,
diff --git a/src/client/views/collections/FlashcardPracticeUI.scss b/src/client/views/collections/FlashcardPracticeUI.scss
index 4ed27793d..210c6798f 100644
--- a/src/client/views/collections/FlashcardPracticeUI.scss
+++ b/src/client/views/collections/FlashcardPracticeUI.scss
@@ -1,3 +1,9 @@
+.FlashcardPracticeUI {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+}
.FlashcardPracticeUI-remove,
.FlashcardPracticeUI-check {
position: absolute;
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 7697d308b..c298bff22 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -1,19 +1,19 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
+import { MultiToggle, Type } from 'browndash-components';
import { computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { returnFalse, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
import { Doc, DocListCast } from '../../../fields/Doc';
import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { ObservableReactComponent } from '../ObservableReactComponent';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import './FlashcardPracticeUI.scss';
-import { IconButton, MultiToggle, Type } from 'browndash-components';
-import { SnappingManager } from '../../util/SnappingManager';
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { emptyFunction } from '../../../Utils';
export enum practiceMode {
PRACTICE = 'practice',
@@ -24,6 +24,11 @@ enum practiceVal {
CORRECT = 'correct',
}
+export enum flashcardRevealOp {
+ FLIP = 'flip',
+ SLIDE = 'slide',
+}
+
interface PracticeUIProps {
fieldKey: string;
layoutDoc: Doc;
@@ -53,7 +58,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore
@computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
- @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._layout_isFlashcard) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+ @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale);
btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
@@ -99,8 +104,8 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
const setPracticeVal = (e: React.MouseEvent, val: string) => {
e.stopPropagation();
const curDoc = this._props.curDoc();
- curDoc && (curDoc[this.practiceField] = val);
this._props.advance?.(val === practiceVal.CORRECT);
+ curDoc && (curDoc[this.practiceField] = val);
};
return this.practiceMode == practiceMode.PRACTICE && this._props.curDoc() ? (
@@ -122,11 +127,11 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
const setColor = (mode: practiceMode) => (StrCast(this.practiceMode) === mode ? 'white' : 'lightgray');
const togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
- return !this._props.allChildDocs().some(doc => doc._layout_isFlashcard) ? null : (
+ return !this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? null : (
<div
className="FlashcardPracticeUI-practiceModes"
style={{
- transform: `translateY(${(this.btnHeight() * (1 - Math.min(1, this._props.uiBtnScaling))) / this._props.ScreenToLocalBoxXf().Scale}px)`,
+ transform: this._props.ScreenToLocalBoxXf().Scale >= 1 ? undefined : `translateY(${this.btnHeight() / this._props.ScreenToLocalBoxXf().Scale - this.btnHeight()}px)`,
}}>
<MultiToggle
tooltip="Practice flashcards one at a time"
@@ -148,28 +153,36 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
selectedItems={this.practiceMode}
onSelectionChange={(val: (string | number) | (string | number)[]) => togglePracticeMode(val as practiceMode)}
/>
- <IconButton
- tooltip="hover over card to reveal answer"
- type={Type.TERT}
- text={StrCast(this._props.layoutDoc.revealOp)}
+ <MultiToggle
+ tooltip="How to reveal flashcard answer"
+ type={Type.PRIM}
color={SnappingManager.userColor}
background={SnappingManager.userVariantColor}
- icon={<FontAwesomeIcon color={SnappingManager.userColor} icon={this._props.layoutDoc.revealOp === 'hover' ? 'hand-point-up' : 'question'} size="sm" />}
- label={StrCast(this._props.layoutDoc.revealOp)}
- onPointerDown={e =>
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => {
- this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === 'hover' ? 'flip' : 'hover';
- this._props.layoutDoc.childDocumentsActive = this._props.layoutDoc.revealOp === 'hover' ? true : undefined;
- })
- }
+ multiSelect={false}
+ isToggle={false}
+ toggleStatus={!!this.practiceMode}
+ label={StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)}
+ items={[
+ ['reveal', StrCast(this._props.layoutDoc.revealOp) === flashcardRevealOp.SLIDE ? 'expand' : 'question', StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)],
+ ['trigger', this._props.layoutDoc.revealOp_hover ? 'hand-point-up' : 'hand', this._props.layoutDoc.revealOp_hover ? 'show on hover' : 'show on click'],
+ ].map(([item, icon, tooltip]) => ({
+ icon: <FontAwesomeIcon className={`FlashcardPracticeUI-${item}`} color={setColor(item as practiceMode)} icon={icon as IconProp} size="sm" />,
+ tooltip: tooltip,
+ val: item,
+ }))}
+ selectedItems={this._props.layoutDoc.revealOp_hover ? ['reveal', 'trigger'] : 'reveal'}
+ onSelectionChange={(val: (string | number) | (string | number)[]) => {
+ if (val === 'reveal') this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === flashcardRevealOp.SLIDE ? flashcardRevealOp.FLIP : flashcardRevealOp.SLIDE;
+ if (val === 'trigger') this._props.layoutDoc.revealOp_hover = !this._props.layoutDoc.revealOp_hover;
+ }}
/>
</div>
);
}
- tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
+ tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._flashcardType) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
render() {
return (
- <>
+ <div className="FlashcardPracticeUI">
{this.emptyMessage}
{this.practiceButtons}
{this._props.layoutDoc._chromeHidden ? null : (
@@ -195,7 +208,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
{this.practiceModesMenu}
</div>
)}
- </>
+ </div>
);
}
}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 5bfdee1f5..60728877a 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -285,6 +285,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
static Activate = (tabDoc: Doc) => {
const tab = Array.from(CollectionDockingView.Instance?.tabMap ?? []).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue);
+ if (tab && tab.header.parent._activeContentItem === tab.contentItem) return false;
tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
return tab !== undefined;
};
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
index 6ad67a864..8530f7a23 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
@@ -1,4 +1,4 @@
-import { action, observable } from 'mobx';
+import { action, observable, untracked } from 'mobx';
import { CollectionFreeFormView } from '.';
import { intersectRect } from '../../../../Utils';
import { Doc, Opt } from '../../../../fields/Doc';
@@ -179,7 +179,9 @@ export class CollectionFreeFormClusters {
};
styleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => {
- if (doc && this.childDocs?.includes(doc))
+ // without untracked, every inquired style property for any Doc will be invalidated if a change is made to the collection's childDocs.
+ // this prevents that by assuming that a Doc is generally always (or never) a member of childDocs - if it's removed or added, then all of its properties get updated anyway.
+ if (doc && untracked(() => this.childDocs)?.includes(doc))
switch (property.split(':')[0]) {
case StyleProp.BackgroundColor:
{
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
index 5d8373fc7..8b9a3e0ec 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
@@ -29,7 +29,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
}
_firstDocPos = { x: 0, y: 0 };
- constructor(props: any) {
+ constructor(props: CollectionFreeFormInfoUIProps) {
super(props);
makeObservable(this);
this._currState = this.setupStates();
@@ -163,7 +163,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
return presentDocs;
}],
// eslint-disable-next-line no-use-before-define
- activePen: [() => activeTool() === InkTool.Pen, () => penMode],
+ activePen: [() => activeTool() === InkTool.Ink, () => penMode],
},
'documentation.png',
() => TopBar.Instance.FlipDocumentationIcon()
@@ -187,7 +187,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
const penMode = InfoState('You\'re in pen mode. Click and drag to draw your first masterpiece.', {
// activePen: [() => activeTool() === InkTool.Eraser, () => eraserMode],
- activePen: [() => activeTool() !== InkTool.Pen, () => viewedLink],
+ activePen: [() => activeTool() !== InkTool.Ink, () => viewedLink],
}); // prettier-ignore
// const eraserMode = InfoState('You\'re in eraser mode. Say goodbye to your first masterpiece.', {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 8d73601d1..65b2702c6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -10,7 +10,7 @@ import { DateField } from '../../../../fields/DateField';
import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
import { DocData, Height, Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
-import { InkData, InkField, InkTool, Segment } from '../../../../fields/InkField';
+import { InkData, InkEraserTool, InkField, InkInkTool, InkTool, Segment } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { RichTextField } from '../../../../fields/RichTextField';
import { listSpec } from '../../../../fields/Schema';
@@ -36,11 +36,24 @@ import { ContextMenu } from '../../ContextMenu';
import { InkingStroke } from '../../InkingStroke';
import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveEraserWidth, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, SetActiveInkColor, SetActiveInkWidth } from '../../nodes/DocumentView';
+import {
+ ActiveInkArrowEnd,
+ ActiveInkArrowStart,
+ ActiveInkDash,
+ ActiveEraserWidth,
+ ActiveInkFillColor,
+ ActiveInkBezierApprox,
+ ActiveInkColor,
+ ActiveInkWidth,
+ ActiveIsInkMask,
+ DocumentView,
+ SetActiveInkColor,
+ SetActiveInkWidth,
+} from '../../nodes/DocumentView';
import { FieldViewProps } from '../../nodes/FieldView';
import { FocusViewOptions } from '../../nodes/FocusViewOptions';
import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { OpenWhere, OpenWhereMod } from '../../nodes/OpenWhere';
+import { OpenWhere } from '../../nodes/OpenWhere';
import { PinDocView, PinProps } from '../../PinFuncs';
import { StickerPalette } from '../../smartdraw/StickerPalette';
import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
@@ -389,7 +402,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return undefined;
};
- getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
+ getView = (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
new Promise<Opt<DocumentView>>(res => {
if (doc.hidden && this._lightboxDoc !== doc) options.didMove = !(doc.hidden = false);
if (doc === this.Document) {
@@ -497,13 +510,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
// prettier-ignore
const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
switch (Doc.ActiveTool) {
- case InkTool.Highlighter:
- case InkTool.Write:
- case InkTool.Pen:
+ case InkTool.Ink:
break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
- case InkTool.StrokeEraser:
- case InkTool.SegmentEraser:
- case InkTool.RadiusEraser:
+ case InkTool.Eraser:
this._batch = UndoManager.StartBatch('collectionErase');
this._eraserPts.length = 0;
setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, this.onEraserClick, hit !== -1);
@@ -543,7 +552,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const { points } = ge;
const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
const inkDoc = this.createInkDoc(points, B);
- if (Doc.ActiveTool === InkTool.Write) {
+ if (Doc.ActiveInk === InkInkTool.Highlight) inkDoc[DocData].backgroundColor = 'transparent';
+ if (Doc.ActiveInk === InkInkTool.Write) {
this.unprocessedDocs.push(inkDoc);
CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
}
@@ -606,7 +616,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const currPoint = { X: e.clientX, Y: e.clientY };
this._eraserPts.push([currPoint.X, currPoint.Y]);
this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
- if (Doc.ActiveTool === InkTool.RadiusEraser) {
+ if (Doc.ActiveEraser === InkEraserTool.Radius) {
const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
strokeMap.forEach((intersects, stroke) => {
if (!this._deleteList.includes(stroke)) {
@@ -614,13 +624,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
const segments = this.radiusErase(stroke, intersects.sort());
- segments?.forEach(segment =>
- this.forceStrokeGesture(
- e,
- Gestures.Stroke,
- segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
- )
- );
+ segments?.forEach(segment => {
+ const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
+ const bounds = InkField.getBounds(points);
+ const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+ const inkDoc = this.createInkDoc(points, B);
+ ['color', 'fillColor', 'stroke_width', 'stroke_dash', 'stroke_bezier'].forEach(field => {
+ inkDoc[DocData][field] = stroke.dataDoc[field];
+ });
+ this.addDocument(inkDoc);
+ });
}
stroke.layoutDoc.opacity = 0;
stroke.layoutDoc.dontIntersect = true;
@@ -632,7 +645,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1');
SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black');
// create a new curve by appending all curves of the current segment together in order to render a single new stroke.
- if (Doc.ActiveTool !== InkTool.StrokeEraser) {
+ if (Doc.ActiveEraser !== InkEraserTool.Stroke) {
// this._eraserLock++;
const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
const newStrokes = segments?.map(segment => {
@@ -1195,14 +1208,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
y: B.y - inkWidth / 2,
_width: B.width + inkWidth,
_height: B.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
inkWidth,
ActiveInkColor(),
ActiveInkBezierApprox(),
- ActiveFillColor(),
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
+ ActiveInkFillColor(),
+ ActiveInkArrowStart(),
+ ActiveInkArrowEnd(),
+ ActiveInkDash(),
ActiveIsInkMask()
);
};
@@ -1235,14 +1248,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
y: B.y - inkWidth / 2,
_width: B.width + inkWidth,
_height: B.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ stroke_showLabel: BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
inkWidth,
opts.autoColor ? stroke[1] : ActiveInkColor(),
ActiveInkBezierApprox(),
- stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
+ stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2],
+ ActiveInkArrowStart(),
+ ActiveInkArrowEnd(),
+ ActiveInkDash(),
ActiveIsInkMask()
);
this._drawing.push(inkDoc);
@@ -1596,21 +1609,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
case undefined:
case OpenWhere.lightbox:
- {
- const firstDoc = docs[0];
- if (this.layoutDoc._isLightbox) {
- this._lightboxDoc = firstDoc;
- return true;
- }
- if (firstDoc === this.Document || this.childDocList?.includes(firstDoc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(firstDoc)) {
- if (firstDoc.hidden) firstDoc.hidden = false;
- if (!location.includes(OpenWhereMod.always)) return true;
- }
+ if (this.layoutDoc._isLightbox) {
+ this._lightboxDoc = docs[0];
+ return true;
}
- break;
+ return this.addLinkedDocTab(docsIn, location);
default:
}
- return this._props.addDocTab(docs, location);
+ return this._props.addDocTab(docsIn, location);
});
getCalculatedPositions(pair: { layout: Doc; data?: Doc }): PoolData {
const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min);
@@ -1750,7 +1756,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
});
PinDocView(
anchor,
- { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } },
+ { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, collectionType: true, filters: true } },
this.Document
);
@@ -2202,7 +2208,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
width: `${100 / this.nativeDimScaling}%`,
height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
}}>
- {Doc.ActiveTool === InkTool.RadiusEraser && this._showEraserCircle && (
+ {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
<div
onPointerMove={this.onCursorMove}
style={{
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index 534f67927..6d51ecac6 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -8,7 +8,7 @@ import { observer } from 'mobx-react';
import React from 'react';
import { DivHeight, lightOrDark, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
import { emptyFunction } from '../../../../Utils';
-import { Doc, Opt } from '../../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
import { DocData } from '../../../../fields/DocSymbols';
import { List } from '../../../../fields/List';
import { DocCast, ImageCast, NumCast, StrCast } from '../../../../fields/Types';
@@ -54,7 +54,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable _headerRef: HTMLDivElement | null = null;
@observable _listRef: HTMLDivElement | null = null;
- observer = new ResizeObserver(a => {
+ observer = new ResizeObserver(() => {
this._props.setHeight?.(
(this.props.Document._face_showImages ? 20 : 0) + //
(!this._headerRef ? 0 : DivHeight(this._headerRef)) +
@@ -97,9 +97,9 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
const faceMatcher = new FaceMatcher([labeledFaceDescriptor], 1);
const faceAnno =
FaceRecognitionHandler.ImageDocFaceAnnos(imgDoc).reduce(
- (prev, faceAnno) => {
- const match = faceMatcher.matchDescriptor(new Float32Array(Array.from(faceAnno.faceDescriptor as List<number>)));
- return match.distance < prev.dist ? { dist: match.distance, faceAnno } : prev;
+ (prev, fAnno) => {
+ const match = faceMatcher.matchDescriptor(new Float32Array(Array.from(fAnno.faceDescriptor as List<number>)));
+ return match.distance < prev.dist ? { dist: match.distance, faceAnno: fAnno } : prev;
},
{ dist: 1, faceAnno: undefined as Opt<Doc> }
).faceAnno ?? imgDoc;
@@ -108,10 +108,18 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (faceAnno) {
faceAnno.face && FaceRecognitionHandler.UniqueFaceRemoveFaceImage(faceAnno, DocCast(faceAnno.face));
FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, this.Document);
- faceAnno.face = this.Document;
+ faceAnno[DocData].face = this.Document[DocData];
}
}
});
+ de.complete.docDragData?.droppedDocuments
+ ?.filter(doc => DocCast(doc.face)?.type === DocumentType.UFACE)
+ .forEach(faceAnno => {
+ const imgDoc = faceAnno;
+ faceAnno.face && FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, DocCast(faceAnno.face));
+ FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, this.Document);
+ faceAnno[DocData].face = this.Document[DocData];
+ });
e.stopPropagation();
return true;
}
@@ -189,7 +197,8 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
this,
e,
() => {
- DragManager.StartDocumentDrag([e.target as HTMLElement], new DragManager.DocumentDragData([doc], dropActionType.embed), e.clientX, e.clientY);
+ const dragDoc = DocListCast(doc.data_annotations).find(a => a.face === this.Document[DocData]) ?? this.Document;
+ DragManager.StartDocumentDrag([e.target as HTMLElement], new DragManager.DocumentDragData([dragDoc], dropActionType.embed), e.clientX, e.clientY);
return true;
},
emptyFunction,
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index c865c681d..ddc50871d 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -690,7 +690,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
}}
style={{
overflow: StrCast(this._props.Document._overflow),
- cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer',
+ cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
}}
onDragOver={e => e.preventDefault()}
onScroll={e => {
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 6ffb0865a..7f519b065 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -188,7 +188,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
selectedCells={this.selectedCells}
selectedCol={this.selectedCol}
setColumnValues={this.setColumnValues}
- oneLine={BoolCast(this.schemaDoc?._singleLine)}
+ oneLine={BoolCast(this.schemaDoc?._schema_singleLine)}
menuTarget={this.schemaView.MenuTarget}
transform={() => {
const ind = index === this.schemaView.columnKeys.length - 1 ? this.schemaView.columnKeys.length - 3 : index;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 903e04ad7..40144c4ce 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -3,7 +3,7 @@ import { Colors } from 'browndash-components';
import { runInAction } from 'mobx';
import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
-import { InkTool } from '../../../fields/InkField';
+import { InkEraserTool, InkInkTool, InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
@@ -16,21 +16,20 @@ import { SnappingManager } from '../../util/SnappingManager';
import { UndoManager, undoable } from '../../util/UndoManager';
import { GestureOverlay } from '../GestureOverlay';
import { InkTranscription } from '../InkTranscription';
-import { InkingStroke } from '../InkingStroke';
import { PropertiesView } from '../PropertiesView';
import { CollectionFreeFormView } from '../collections/collectionFreeForm';
import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
import {
ActiveEraserWidth,
- ActiveFillColor,
+ ActiveInkFillColor,
ActiveInkColor,
- ActiveInkHideTextLabels,
+ ActiveHideTextLabels,
ActiveInkWidth,
ActiveIsInkMask,
DocumentView,
- SetActiveFillColor,
+ SetActiveInkFillColor,
SetActiveInkColor,
- SetActiveInkHideTextLabels,
+ SetactiveHideTextLabels,
SetActiveInkWidth,
SetActiveIsInkMask,
SetEraserWidth,
@@ -60,20 +59,21 @@ ScriptingGlobals.add(function setView(view: string, getSelected: boolean) {
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
const selectedViews = DocumentView.Selected();
+ const selectedDoc = selectedViews.lastElement()?.Document;
+ const defaultFill = selectedDoc?._layout_isSvg ? () => StrCast(selectedDoc[DocData].fillColor) : !Doc.ActiveTool || Doc.ActiveTool === InkTool.None ? () => StrCast(Doc.UserDoc().textBackgroundColor, 'transparent') : () => ActiveInkFillColor();
+ const setDefaultFill = !Doc.ActiveTool || Doc.ActiveTool === InkTool.None ? (c: string) => { Doc.UserDoc().textBackgroundColor = c; }: SetActiveInkFillColor; // prettier-ignore
if (Doc.ActiveTool !== InkTool.None && !selectedViews.lastElement()?.Document._layout_isSvg) {
- if (checkResult) {
- return ActiveFillColor();
- }
- SetActiveFillColor(color ?? 'transparent');
+ if (checkResult) return defaultFill();
+ setDefaultFill(color ?? 'transparent');
} else if (selectedViews.length) {
if (checkResult) {
const selView = selectedViews.lastElement();
const fieldKey = selView.Document._layout_isSvg ? 'fillColor' : 'backgroundColor';
const layoutFrameNumber = Cast(selView.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
const contentFrameNumber = Cast(selView.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
- return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] ?? 'transparent';
+ return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] || defaultFill();
}
- selectedViews.some(dv => dv.ComponentView instanceof InkingStroke) && SetActiveFillColor(color ?? 'transparent');
+ setDefaultFill(color ?? 'transparent');
selectedViews.forEach(dv => {
const fieldKey = dv.Document._layout_isSvg ? 'fillColor' : 'backgroundColor';
const layoutFrameNumber = Cast(dv.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
@@ -92,10 +92,13 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
} else {
const selected = DocumentView.SelectedDocs().length ? DocumentView.SelectedDocs() : LinkManager.Instance.currentLink ? [LinkManager.Instance.currentLink] : [];
if (checkResult) {
- return selected.lastElement()?._backgroundColor ?? 'transparent';
+ return selected.lastElement()?._backgroundColor ?? defaultFill();
}
- SetActiveFillColor(color ?? 'transparent');
- selected.forEach(doc => { doc[DocData].backgroundColor = color; }); // prettier-ignore
+ if (!selected.length) setDefaultFill(color ?? 'transparent');
+ else
+ selected.forEach(doc => {
+ doc[DocData][doc._layout_isSvg ? 'fillColor' : 'backgroundColor'] = color;
+ });
}
return '';
});
@@ -189,38 +192,38 @@ ScriptingGlobals.add(function showFreeform(
setDoc: (doc: Doc, dv: DocumentView) => { Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards}, // prettier-ignore
}],
['time', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "time",
- setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "time" ? doc.cardSort = '' : doc.cardSort = 'time'}, // prettier-ignore
+ checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "time",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "time" ? doc.card_sort = '' : doc.card_sort = 'time'}, // prettier-ignore
}],
['docType', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "type",
- setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "type" ? doc.cardSort = '' : doc.cardSort = 'type'}, // prettier-ignore
+ checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "type",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "type" ? doc.card_sort = '' : doc.card_sort = 'type'}, // prettier-ignore
}],
['color', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "color",
- setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "color" ? doc.cardSort = '' : doc.cardSort = 'color'}, // prettier-ignore
+ checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "color",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "color" ? doc.card_sort = '' : doc.card_sort = 'color'}, // prettier-ignore
}],
['tag', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "tag",
- setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "tag" ? doc.cardSort = '' : doc.cardSort = 'tag'}, // prettier-ignore
+ checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "tag",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "tag" ? doc.card_sort = '' : doc.card_sort = 'tag'}, // prettier-ignore
}],
['up', {
- checkResult: (doc: Doc) => BoolCast(!doc?.cardSort_isDesc),
+ checkResult: (doc: Doc) => BoolCast(!doc?.card_sort_isDesc),
setDoc: (doc: Doc, dv: DocumentView) => {
- doc.cardSort_isDesc = false;
+ doc.card_sort_isDesc = false;
},
}],
['down', {
- checkResult: (doc: Doc) => BoolCast(doc?.cardSort_isDesc),
+ checkResult: (doc: Doc) => BoolCast(doc?.card_sort_isDesc),
setDoc: (doc: Doc, dv: DocumentView) => {
- doc.cardSort_isDesc = true;
+ doc.card_sort_isDesc = true;
},
}],
['toggle-chat', {
checkResult: (doc: Doc) => GPTPopup.Instance.visible,
setDoc: (doc: Doc, dv: DocumentView) => {
if (GPTPopup.Instance.visible){
- doc.cardSort = ''
+ doc.card_sort = ''
GPTPopup.Instance.setVisible(false);
} else {
@@ -295,10 +298,10 @@ ScriptingGlobals.add(function setTagFilter(tag: string, added: boolean, checkRes
}, '');
// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize' | 'alignment', value: string | number, checkResult?: boolean) {
+ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: string | number, checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
// prettier-ignore
- const map: Map<'font'|'fontColor'|'highlight'|'fontSize'|'alignment', { checkResult: () => string | undefined; setDoc: () => void;}> = new Map([
+ const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => string | undefined; setDoc: () => void;}> = new Map([
['font', {
checkResult: () => RichTextMenu.Instance?.fontFamily,
setDoc: () => value && RichTextMenu.Instance?.setFontField(value.toString(), 'fontFamily'),
@@ -311,10 +314,6 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
checkResult: () => RichTextMenu.Instance?.fontColor,
setDoc: () => value && RichTextMenu.Instance?.setFontField(value.toString(), 'fontColor'),
}],
- ['alignment', {
- checkResult: () => RichTextMenu.Instance?.textAlign,
- setDoc: () => { value && editorView?.state ? RichTextMenu.Instance?.align(editorView, editorView.dispatch, value.toString() as "center"|"left"|"right"):(Doc.UserDoc().textAlign = value); },
- }],
['fontSize', {
checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''),
setDoc: () => {
@@ -334,7 +333,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
return undefined;
});
-type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
+type attrname = 'noAutoLink' | 'dictation' | 'fitBox' | 'bold' | 'italic' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
type attrfuncs = [attrname, { checkResult: () => boolean; toggle?: () => unknown }];
// eslint-disable-next-line prefer-arrow-callback
@@ -343,14 +342,10 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
const editorView = textView?.EditorView;
// prettier-ignore
const alignments:attrfuncs[] = (['left','right','center','vcent'] as ("left"|"center"|"right"|"vcent")[]).map((where) =>
- [ where, { checkResult: () => editorView ? (where === 'vcent' ? RichTextMenu.Instance?.textVcenter ?? false:
- (RichTextMenu.Instance?.textAlign === where)):
- where === 'vcent' ? BoolCast(Doc.UserDoc()._layout_centered):
- (Doc.UserDoc().textAlign === where),
- toggle: () => { editorView?.state ? (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle():
- RichTextMenu.Instance?.align(editorView, editorView.dispatch, where)):
- where === 'vcent' ? Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered:
- (Doc.UserDoc().textAlign = where); }
+ [ where, { checkResult: () => (where === 'vcent' ? RichTextMenu.Instance?.textVcenter ?? false:
+ (RichTextMenu.Instance?.textAlign === where)),
+ toggle: () => { (where === 'vcent' ? RichTextMenu.Instance?.vcenterToggle():
+ RichTextMenu.Instance?.align(editorView, editorView?.dispatch, where)); }
}]); // prettier-ignore
// prettier-ignore
const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list =>
@@ -360,16 +355,18 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
const attrs:attrfuncs[] = [
['dictation', { checkResult: () => !!textView?._recordingDictation,
toggle: () => textView && runInAction(() => { textView._recordingDictation = !textView._recordingDictation;} ) }],
+ ['fitBox', { checkResult: () => RichTextMenu.Instance?.fitBox ?? false,
+ toggle: () => RichTextMenu.Instance?.toggleFitBox()}],
['elide', { checkResult: () => false,
toggle: () => editorView ? RichTextMenu.Instance?.elideSelection(): 0}],
['noAutoLink',{ checkResult: () => ((editorView && RichTextMenu.Instance?.noAutoLink) ?? false),
toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
['bold', { checkResult: () => (editorView ? RichTextMenu.Instance?.bold??false : (Doc.UserDoc().fontWeight === 'bold')),
toggle: editorView ? RichTextMenu.Instance?.toggleBold : () => { Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold'; }}],
- ['italics', { checkResult: () => (editorView ? RichTextMenu.Instance?.italics ?? false : (Doc.UserDoc().fontStyle === 'italics')),
- toggle: editorView ? RichTextMenu.Instance?.toggleItalics : () => { Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics'; }}],
- ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().textDecoration === 'underline')),
- toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => { Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline'; } }]]
+ ['italic', { checkResult: () => (editorView ? RichTextMenu.Instance?.italic ?? false : (Doc.UserDoc().fontStyle === 'italic')),
+ toggle: editorView ? RichTextMenu.Instance?.toggleItalic : () => { Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italic' ? undefined : 'italic'; }}],
+ ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance?.underline ?? false: (Doc.UserDoc().fontDecoration === 'underline')),
+ toggle: editorView ? RichTextMenu.Instance?.toggleUnderline : () => { Doc.UserDoc().fontDecoration = Doc.UserDoc().fontDecoration === 'underline' ? undefined : 'underline'; } }]]
const map = new Map(attrs.concat(alignments).concat(listings));
if (checkResult) {
@@ -379,43 +376,46 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
return undefined;
});
-function setActiveTool(toolIn: InkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
+function setActiveTool(tool: InkTool | InkEraserTool | InkInkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
InkTranscription.Instance?.createInkGroup();
- const tool = toolIn === InkTool.Eraser ? Doc.UserDoc().activeEraserTool : toolIn;
if (checkResult) {
- return ((Doc.ActiveTool === tool || (Doc.UserDoc().activeEraserTool === tool && (tool === toolIn || Doc.ActiveTool === tool))) && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
- ? GestureOverlay.Instance?.KeepPrimitiveMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures)
+ return Doc.ActiveTool === tool || Doc.ActiveEraser === tool || Doc.ActiveInk === tool || SnappingManager.InkShape === tool
+ ? true //SnappingManager.KeepGestureMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures)
: false;
}
runInAction(() => {
+ const eraserTool = tool === InkTool.Eraser ? Doc.ActiveEraser : [InkEraserTool.Stroke, InkEraserTool.Radius, InkEraserTool.Segment].includes(tool as InkEraserTool) ? (tool as InkEraserTool) : undefined;
+ const inkTool = tool === InkTool.Ink ? Doc.ActiveInk : [InkInkTool.Pen, InkInkTool.Write, InkInkTool.Highlight].includes(tool as InkInkTool) ? (tool as InkInkTool) : undefined;
if (GestureOverlay.Instance) {
- GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
+ SnappingManager.SetKeepGestureMode(keepPrim);
}
if (Object.values(Gestures).includes(tool as Gestures)) {
- if (GestureOverlay.Instance.InkShape === tool && !keepPrim) {
+ if (SnappingManager.InkShape === tool && !keepPrim) {
Doc.ActiveTool = InkTool.None;
- GestureOverlay.Instance.InkShape = undefined;
+ SnappingManager.SetInkShape(undefined);
} else {
- Doc.ActiveTool = InkTool.Pen;
- GestureOverlay.Instance.InkShape = tool as Gestures;
+ Doc.ActiveTool = InkTool.Ink;
+ SnappingManager.SetInkShape(tool as Gestures);
}
- } else if (tool) {
- if (Doc.UserDoc().ActiveTool === tool) {
+ } else if (eraserTool) {
+ if (Doc.ActiveTool === InkTool.Eraser && Doc.ActiveTool === tool) {
Doc.ActiveTool = InkTool.None;
} else {
- if ([InkTool.StrokeEraser, InkTool.RadiusEraser, InkTool.SegmentEraser].includes(tool as InkTool)) {
- Doc.UserDoc().activeEraserTool = tool;
- }
- // pen or eraser
- if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
- Doc.ActiveTool = InkTool.None;
- } else {
- Doc.ActiveTool = tool as InkTool;
- GestureOverlay.Instance.InkShape = undefined;
- }
+ Doc.ActiveEraser = eraserTool;
+ Doc.ActiveTool = InkTool.Eraser;
+ SnappingManager.SetInkShape(undefined);
+ }
+ } else if (inkTool) {
+ if (Doc.ActiveTool === InkTool.Ink && Doc.ActiveTool === tool) {
+ Doc.ActiveTool = InkTool.None;
+ } else {
+ Doc.ActiveInk = inkTool;
+ Doc.ActiveTool = InkTool.Ink;
+ SnappingManager.SetInkShape(undefined);
}
} else {
- Doc.ActiveTool = InkTool.None;
+ if ((Doc.ActiveTool === tool || !tool) && !keepPrim) Doc.ActiveTool = InkTool.None;
+ else Doc.ActiveTool = tool as InkTool;
}
});
return undefined;
@@ -425,44 +425,44 @@ ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function activeEraserTool() {
- return StrCast(Doc.UserDoc().activeEraserTool, InkTool.StrokeEraser);
+ return StrCast(Doc.UserDoc().activeEraserTool, InkEraserTool.Stroke);
}, 'returns the current eraser tool');
// toggle: Set overlay status of selected document
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', value: string | number, checkResult?: boolean) {
- const selected = DocumentView.SelectedDocs().lastElement() ?? Doc.UserDoc();
+ const selected = DocumentView.SelectedDocs().lastElement();
// prettier-ignore
const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', { checkResult: () => number|boolean|string|undefined; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
['inkMask', {
checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_isInkMask) : ActiveIsInkMask())),
setInk: (doc: Doc) => { doc[DocData].stroke_isInkMask = !doc.stroke_isInkMask; },
- setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()),
+ setMode: () => SetActiveIsInkMask(value ? true : false)
}],
['labels', {
- checkResult: () => ((selected?._stroke_showLabel ? BoolCast(selected[DocData].stroke_showLabel) : ActiveInkHideTextLabels())),
- setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = !doc.stroke_showLabel; },
- setMode: () => selected?.type !== DocumentType.INK && SetActiveInkHideTextLabels(!ActiveInkHideTextLabels()),
+ checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_showLabel) : !ActiveHideTextLabels())),
+ setInk: (doc: Doc) => { doc[DocData].stroke_showLabel = value; },
+ setMode: () => SetactiveHideTextLabels(value? false : true),
}],
['fillColor', {
- checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveFillColor() ?? "transparent"),
+ checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveInkFillColor() ?? "transparent"),
setInk: (doc: Doc) => { doc[DocData].fillColor = StrCast(value); },
- setMode: () => SetActiveFillColor(StrCast(value)),
+ setMode: () => SetActiveInkFillColor(StrCast(value)),
}],
[ 'strokeWidth', {
checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width) : ActiveInkWidth()),
setInk: (doc: Doc) => { doc[DocData].stroke_width = NumCast(value); },
- setMode: () => { SetActiveInkWidth(value.toString()); selected?.type === DocumentType.INK && setActiveTool( GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
+ setMode: () => SetActiveInkWidth(value.toString()),
}],
['strokeColor', {
checkResult: () => (selected?._layout_isSvg? StrCast(selected[DocData].color) : ActiveInkColor()),
setInk: (doc: Doc) => { doc[DocData].color = String(value); },
- setMode: () => { SetActiveInkColor(StrCast(value)); selected?.type === DocumentType.INK && setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
+ setMode: () => SetActiveInkColor(StrCast(value))
}],
[ 'eraserWidth', {
checkResult: () => ActiveEraserWidth() === 0 ? 1 : ActiveEraserWidth(),
setInk: (doc: Doc) => { },
- setMode: () => { SetEraserWidth(+value);},
+ setMode: () => SetEraserWidth(+value),
}]
]);
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
index 3eb99f47a..8115bafbf 100644
--- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable jsx-a11y/no-static-element-interactions */
-/* eslint-disable jsx-a11y/click-events-have-key-events */
import { action } from 'mobx';
import * as React from 'react';
import { Doc } from '../../../../fields/Doc';
@@ -35,10 +33,10 @@ export function ButtonMenu() {
<div
className="newLightboxView-penBtn"
title="toggle pen annotation"
- style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }}
+ style={{ background: Doc.ActiveTool === InkTool.Ink ? 'white' : undefined }}
onClick={e => {
e.stopPropagation();
- Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+ Doc.ActiveTool = Doc.ActiveTool === InkTool.Ink ? InkTool.None : InkTool.Ink;
}}
/>
<div
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index d51b1cd3a..a9edc90e7 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,7 +1,8 @@
+import { Colors } from 'browndash-components';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { OmitKeys } from '../../../ClientUtils';
+import { DashColor, OmitKeys } from '../../../ClientUtils';
import { numberRange } from '../../../Utils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { TransitionTimer } from '../../../fields/DocSymbols';
@@ -296,12 +297,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
transition: this.DataTransition(),
zIndex: this.ZIndex,
display: this.Width ? undefined : 'none',
+ mixBlendMode: DashColor(StrCast(this.layoutDoc[this.layoutDoc._layout_isSvg ? 'fillColor' : 'backgroundColor'], Colors.WHITE)).alpha() !== 1 ? 'multiply' : undefined,
}}>
{this.RenderCutoffProvider(this.Document) ? (
<div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} />
) : (
<DocumentView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
Document={this._props.Document}
renderDepth={this._props.renderDepth}
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index c328ef4bf..d2ba9796b 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -236,6 +236,9 @@
}
}
}
+.comparisonBox-interactive {
+ pointer-events: all;
+}
.comparisonBox-explain {
position: absolute;
@@ -246,8 +249,7 @@
pointer-events: none;
}
-.comparisonBox-interactive {
- pointer-events: unset;
+.comparisonBox-slide {
cursor: ew-resize;
.slide-bar {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index ccbe98257..672c189ba 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -8,63 +8,198 @@ import ReactLoading from 'react-loading';
import { imageUrlToBase64, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
import { emptyFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
-import { DocData } from '../../../fields/DocSymbols';
-import { Id } from '../../../fields/FieldSymbols';
+import { Animation, DocData } from '../../../fields/DocSymbols';
import { RichTextField } from '../../../fields/RichTextField';
-import { DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
+import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
import { nullAudio } from '../../../fields/URLField';
import { GPTCallType, gptAPICall, gptImageLabel } from '../../apis/gpt/GPT';
import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
-import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
-import { SnappingManager } from '../../util/SnappingManager';
import { undoable } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
+import { flashcardRevealOp, practiceMode } from '../collections/FlashcardPracticeUI';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
import '../pdf/GPTPopup/GPTPopup.scss';
import './ComparisonBox.scss';
import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { practiceMode } from '../collections/FlashcardPracticeUI';
const API_URL = 'https://api.unsplash.com/search/photos';
+/**
+ * This view serves two distinct functions depending on the revealOp field ('slide' or 'flip)
+ * 1) ('slide') - provides a before/after animated sliding transition between two Docs
+ * 2) ('flip') - provides a question/answer flip between two Docs
+ * And a third function that overrides the first two if the doc's container has its 'practiceMode' set to 'quiz'
+ * 3) ('quiz') - it provides a quiz view that displays a question and a user answer that can be "scored" by GPT
+ * NOTE: this should probably be changed to passing down a prop to the flashcard telling it to render as a quiz.
+ *
+ * In each case, the two docs are stored in the <fieldKey>_front and <fieldKey>_back fields
+ *
+ * For 'flip' and 'slide', the trigger can either be clicking, or hovering as determined by the revealOp_hover field.
+ * For 'quiz' the data of both Docs are shown in a single-view quiz display.
+ *
+ * Users can create a stack of flashcards all at once (only) from an empty flashcard by entering a topic into the front card
+ * and clicking on the flashcard stack button. This will convert the comparision box into a stack of comparison boxes
+ * filled in by GPT about the topic.
+ *
+ */
+
@observer
export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(ComparisonBox, fieldKey);
}
+ /**
+ * Creates a flashcard (or fills in flashcard data to a specified Doc) from a control string containing a question and answer
+ * @param tuple string containing Question:, Answer: and optionally a Keyword:
+ * @param useDoc doc to fill in instead of creating a Doc
+ * @returns the resulting flashcard Doc
+ */
+ public static createFlashcard(tuple3: string, frontKey: string, backKey: string, useDoc?: Doc) {
+ const [qtoken, ktoken, atoken] = [ComparisonBox.qtoken, ComparisonBox.ktoken, ComparisonBox.atoken];
+ const [title, tuple] = tuple3.split(qtoken);
+ const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0];
+ const rest = tuple.replace(question, '');
+ // prettier-ignore
+ const answer = rest.startsWith(ktoken) ? // if keyword comes first,
+ tuple.includes(atoken) ? tuple.split(atoken)[1] : "" : //if tuple includes answer, split at answer and take what's left, otherwise there's no answer
+ rest.includes(ktoken) ? // otherwise if keyword is present it must come after answer,
+ rest.split(ktoken)[0].split(atoken)[1] : // split at keyword and take what comes first and split that at answer and take what's left
+ rest.replace(atoken,""); // finally if there's no keyword, just get rid of answer token and take what's left
+ const keyword = rest.replace(atoken, '').replace(answer, '').replace(ktoken, '').trim();
+ const fillInFlashcard = (img?: Doc) => {
+ 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;
+ return useDoc;
+ }
+ return Docs.Create.FlashcardDocument(title, front, back, { _width: 300, _height: 300 });
+ };
+ return keyword && keyword.toLowerCase() !== 'none' ? ComparisonBox.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard();
+ }
+
+ /**
+ * Create a carousel of flashcards from a GPT response string where questions and answers are given in a format loosely defined by:
+ * Question: ... Answer: ... Keyword: ...
+ * Note that Keyword or Answer may not be present, or their orders may be reversed.
+ */
+ public static createFlashcardDeck(text: string, width: number, height: number, front: string, back: string) {
+ return Promise.all(
+ text
+ .toLowerCase()
+ .split(ComparisonBox.ttoken)
+ .filter(t => t)
+ .map(tuple => ComparisonBox.createFlashcard(tuple, front, back))
+ ).then(docs => {
+ return Docs.Create.CarouselDocument(docs, {
+ title: text,
+ _width: width,
+ _height: height,
+ _layout_fitWidth: false,
+ _layout_autoHeight: true,
+ _xMargin: 5,
+ _yMargin: 5,
+ });
+ });
+ }
private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+
+ static qtoken = 'question: ';
+ static ktoken = 'keyword: ';
+ static atoken = 'answer: ';
+ static ttoken = 'title: ';
+ private _slideTiming = 200;
+ private _sideBtnWidth = 35;
private _closeRef = React.createRef<HTMLDivElement>();
- private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined];
- private _reactDisposer: IReactionDisposer | undefined;
- constructor(props: FieldViewProps) {
- super(props);
- makeObservable(this);
- }
+ private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {};
+ private _reactDisposer: { [key: string]: IReactionDisposer } = {};
@observable private _inputValue = '';
@observable private _outputValue = '';
@observable private _loading = false;
- @observable private _isEmpty = false;
@observable private _childActive = false;
@observable private _animating = '';
@observable private _listening = false;
- @observable private _frontSide = false;
- @observable recognition = new this.SpeechRecognition();
+ @observable private _renderSide = this.frontKey;
+ @observable private _recognition = new this.SpeechRecognition();
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ componentDidMount() {
+ this._props.setContentViewBox?.(this);
+ this._reactDisposer.select = reaction(
+ () => this._props.isSelected(),
+ selected => {
+ if (selected && this.revealOp !== flashcardRevealOp.SLIDE) this.activateContent();
+ !selected && (this._childActive = false);
+ }, // what it should update to
+ { fireImmediately: true }
+ );
+ this._reactDisposer.hover = reaction(
+ () => this._props.isContentActive(),
+ hover => {
+ if (!hover) {
+ this.revealOp === flashcardRevealOp.FLIP && this.animateFlipping(this.frontKey);
+ this.revealOp === flashcardRevealOp.SLIDE && this.animateSliding(this._props.PanelWidth() - 3);
+ }
+ }, // what it should update to
+ { fireImmediately: true }
+ );
+ }
+
+ componentWillUnmount() {
+ Object.values(this._reactDisposer).forEach(disposer => disposer?.());
+ }
+
+ protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => {
+ this._disposers[fieldKey]?.();
+ if (ele) {
+ this._disposers[fieldKey] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc);
+ }
+ };
+
+ private internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
+ if (dropEvent.complete.docDragData) {
+ const { droppedDocuments } = dropEvent.complete.docDragData;
+ const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(toList(doc).lastElement(), fieldKey));
+ Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc);
+ !added && e.preventDefault();
+ e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
+ // this.childActive = false;
+ return added;
+ }
+ return undefined;
+ }, 'internal drop');
+ @computed get containerDoc() { return this._props.docViewPath().slice(-2)[0]?.Document; } // prettier-ignore
+ @computed get isQuizMode() { return this.containerDoc?.practiceMode === practiceMode.QUIZ; } // prettier-ignore
+ @computed get isFlashcard() { return StrCast(this.Document.layout_flashcardType); } // prettier-ignore
+ @computed get frontKey() { return this._props.fieldKey + '_front'; } // prettier-ignore
+ @computed get backKey() { return this._props.fieldKey + '_back'; } // prettier-ignore
+ @computed get frontText() { return RTFCast(DocCast(this.dataDoc[this.frontKey]).text)?.Text; } // prettier-ignore
+ @computed get backText() { return RTFCast(DocCast(this.dataDoc[this.backKey]).text)?.Text; } // prettier-ignore
@computed get revealOpKey() { return `_${this._props.fieldKey}_revealOp`; } // prettier-ignore
@computed get clipHeightKey() { return `_${this._props.fieldKey}_clipHeight`; } // prettier-ignore
@computed get clipWidthKey() { return `_${this._props.fieldKey}_clipWidth`; } // prettier-ignore
- @computed get clipWidth() { return NumCast(this.layoutDoc[this.clipWidthKey], 50); } // prettier-ignore
+ @computed get clipWidth() { return NumCast(this.layoutDoc[this.clipWidthKey], this.isFlashcard ? 100: 50); } // prettier-ignore
@computed get clipHeight() { return NumCast(this.layoutDoc[this.clipHeightKey], 200); } // prettier-ignore
- @computed get revealOp() { return StrCast(this.layoutDoc[this.revealOpKey], StrCast(this._props.docViewPath().slice(-2)[0]?.Document.revealOp)); } // prettier-ignore
- set revealOp(value:string) { this.layoutDoc[this.revealOpKey] = value; } // prettier-ignore
+ @computed get revealOp() { return StrCast(this.layoutDoc[this.revealOpKey], StrCast(this.containerDoc?.revealOp, this.isFlashcard ? flashcardRevealOp.FLIP : flashcardRevealOp.SLIDE)) as flashcardRevealOp; } // prettier-ignore
+ @computed get revealOpHover() { return BoolCast(this.layoutDoc[this.revealOpKey+"_hover"], BoolCast(this.containerDoc?.revealOp_hover)); } // prettier-ignore
+ @computed get loading() { return this._loading; } // prettier-ignore
+ set loading(value) { runInAction(() => { this._loading = value; })} // prettier-ignore
@computed get overlayAlternateIcon() {
return (
@@ -73,14 +208,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
className="comparisonBox-alternateButton ccomparisonBox-button"
onPointerDown={e =>
setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
- if (!this.revealOp || this.revealOp === 'flip') {
- this.flipFlashcard();
+ if (!this.revealOp || this.revealOp === flashcardRevealOp.FLIP) {
+ this.animateFlipping();
}
})
}
style={{
- background: this.revealOp === 'hover' ? 'gray' : this._frontSide ? 'white' : 'black',
- color: this.revealOp === 'hover' ? 'black' : this._frontSide ? 'black' : 'white',
+ background: this.revealOpHover ? 'gray' : this._renderSide === this.backKey ? 'white' : 'black',
+ color: this.revealOpHover ? 'black' : this._renderSide === this.backKey ? 'black' : 'white',
display: 'inline-block',
}}>
<FontAwesomeIcon icon="turn-up" size="xl" />
@@ -88,8 +223,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
</Tooltip>
);
}
-
- _sideBtnWidth = 35;
/**
* How much the content of the view is being scaled based on its nesting and its fit-to-width settings
*/
@@ -104,57 +237,49 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
@computed get uiBtnScaling() { return Math.max(this.maxWidgetSize / this._sideBtnWidth, 1) * Math.min(1, this.viewScaling)* (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
@computed get flashcardMenu() {
- return SnappingManager.HideDecorations ? null : (
+ return (
<div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaling})` }}>
- {this.revealOp === 'hover' || !this._props.isSelected() ? null : this.overlayAlternateIcon}
- {!this._props.isSelected() ? null : (
- <>
- {!this._frontSide ? null : (
- <Tooltip
- title={
- <div className="dash-tooltip">{
- !this._frontSide ? "Flip to front side to use GPT":
- "Ask GPT to create an answer on the back side of the flashcard based on your question on the front"}
- </div> // prettier-ignore
- }>
- <div className="comparisonBox-button" onPointerDown={() => (this._frontSide ? this.findImageTags() : null)}>
- <FontAwesomeIcon icon="lightbulb" size="xl" />
- </div>
- </Tooltip>
- )}
- {DocCast(this.Document.embedContainer)?.type_collection !== CollectionViewType.Freeform ? null : (
- <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
- <div className="comparisonBox-button" onClick={this.gptFlashcardPile}>
- <FontAwesomeIcon icon="layer-group" size="xl" />
- </div>
- </Tooltip>
- )}
- </>
+ {this.revealOpHover || !this._props.isSelected() ? null : this.overlayAlternateIcon}
+ {!this._props.isSelected() || this._renderSide === this.frontKey ? null : (
+ <Tooltip title={<div className="dash-tooltip">Ask GPT to create an answer for the question on the front</div>}>
+ <div className="comparisonBox-button" onPointerDown={() => this.askGPT(GPTCallType.CHATCARD)}>
+ <FontAwesomeIcon icon="lightbulb" size="xl" />
+ </div>
+ </Tooltip>
+ )}
+ {!this._props.isSelected() || this._renderSide === this.backKey || !CollectionFreeFormView.from(this.DocumentView?.()) || (this.dataDoc[this.backKey] && !DocCast(this.dataDoc[this.backKey])?.text_placeholder) ? null : (
+ <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
+ <div
+ className="comparisonBox-button"
+ onClick={() =>
+ this.askGPT(GPTCallType.STACK).then(async text => {
+ const newCol = await ComparisonBox.createFlashcardDeck(text, NumCast(this.layoutDoc._width, 250) + 50, NumCast(this.layoutDoc._height, 200), this.frontKey, this.backKey);
+ newCol.x = NumCast(this.layoutDoc.x);
+ newCol.y = NumCast(this.layoutDoc.y);
+ this._props.DocumentView?.()._props.addDocument?.(newCol);
+ this._props.removeDocument?.(this.Document);
+ })
+ }>
+ <FontAwesomeIcon icon="layer-group" size="xl" />
+ </div>
+ </Tooltip>
)}
</div>
);
}
- @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
- this._inputValue = e.target.value;
- };
-
@action activateContent = () => {
this._childActive = true;
};
- @action handleRenderClick = () => {
- this._frontSide = !this._frontSide;
- };
-
@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.QUIZ);
- this._frontSide = false;
- this._outputValue = '';
};
onPointerMove = ({ movementX }: PointerEvent) => {
@@ -165,38 +290,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
return false;
};
- componentDidMount() {
- this._props.setContentViewBox?.(this);
- this._reactDisposer = reaction(
- () => this._props.isSelected(), // when this reaction should update
- selected => !selected && (this._childActive = false) // what it should update to
- );
- }
-
- componentWillUnmount() {
- this._reactDisposer?.();
- }
-
- protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => {
- this._disposers[disposerId]?.();
- if (ele) {
- this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc);
- }
- };
-
- private internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
- if (dropEvent.complete.docDragData) {
- const { droppedDocuments } = dropEvent.complete.docDragData;
- const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(toList(doc).lastElement(), fieldKey));
- Doc.SetContainer(droppedDocuments.lastElement(), this.dataDoc);
- !added && e.preventDefault();
- e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
- // this.childActive = false;
- return added;
- }
- return undefined;
- }, 'internal drop');
-
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
const anchor = Docs.Create.ConfigDocument({
title: 'CompareAnchor:' + this.Document.title,
@@ -219,13 +312,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc);
addDoc = (doc: Doc, which: string) => {
- if (this.dataDoc[which] && !this._isEmpty) return false;
this.dataDoc[which] = doc;
return true;
};
remDoc = (doc: Doc, which: string) => {
if (this.dataDoc[which] === doc) {
- this._isEmpty = true;
this.dataDoc[which] = undefined;
return true;
}
@@ -253,10 +344,30 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
default: return this._props.styleProvider?.(doc, props, property);
} // prettier-ignore
};
- moveDoc1 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true);
- moveDoc2 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true);
- remDoc1 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true);
- remDoc2 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
+ moveDocFront = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.frontKey), true);
+ moveDocBack = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.backKey), true);
+ remDocFront = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.frontKey), true);
+ remDocBack = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.backKey), true);
+ animateSliding = action((targetWidth: number) => {
+ this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth
+ this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth();
+ setTimeout(action(() => {this._animating = ''; }), this._slideTiming); // prettier-ignore
+ });
+
+ _flipAnim: NodeJS.Timeout | undefined;
+ animateFlipping = action((side?: string) => {
+ if (side !== this._renderSide) {
+ this._renderSide = side ?? (this._renderSide === this.frontKey ? this.backKey : this.frontKey); // switches to new front
+ this._animating = '0'; // reveals old front on the bottom layer by making top layer transparent
+ setTimeout(
+ action(() => {
+ this._animating = `all ${this._slideTiming * 5}ms`; // makes new front fade in
+ clearTimeout(this._flipAnim);
+ this._flipAnim = setTimeout( action(() => { this._animating = ''; }), this._slideTiming * 5 ); // prettier-ignore
+ })
+ );
+ }
+ });
registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => {
if (e.button !== 2) {
@@ -268,25 +379,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
action((moveEv, doubleTap) => {
if (doubleTap) {
this._childActive = true;
- if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
- if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+ if (!this.dataDoc[this.frontKey] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.frontKey] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+ if (!this.dataDoc[this.backKey] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.backKey] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
}
}),
false,
undefined,
- action(() => {
- if (this._childActive) return;
- this._animating = 'all 200ms';
- // on click, animate slider movement to the targetWidth
- this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth();
-
- setTimeout(
- action(() => {
- this._animating = '';
- }),
- 200
- );
- })
+ action(() => !this._childActive && this.animateSliding(targetWidth))
);
}
};
@@ -296,26 +395,26 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
*/
setListening = () => {
if (this.SpeechRecognition) {
- this.recognition.continuous = true;
- this.recognition.interimResults = true;
- this.recognition.lang = 'en-US';
- this.recognition.onresult = this.handleResult.bind(this);
+ this._recognition.continuous = true;
+ this._recognition.interimResults = true;
+ this._recognition.lang = 'en-US';
+ this._recognition.onresult = this.handleResult.bind(this);
}
ContextMenu.Instance.setLangIndex(0);
};
startListening = () => {
- this.recognition.start();
+ this._recognition.start();
this._listening = true;
};
stopListening = () => {
- this.recognition.stop();
+ this._recognition.stop();
this._listening = false;
};
setLanguage = (language: string, ind: number) => {
- this.recognition.lang = language;
+ this._recognition.lang = language;
ContextMenu.Instance.setLangIndex(ind);
};
@@ -324,7 +423,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
* @returns
*/
convertAbr = () => {
- switch (this.recognition.lang) {
+ switch (this._recognition.lang) {
case 'en-US': return 'English'; //prettier-ignore
case 'es-ES': return 'Spanish'; //prettier-ignore
case 'fr-FR': return 'French'; //prettier-ignore
@@ -360,17 +459,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
* Gets the transcription of an audio recording by sending the
* recording to backend.
*/
- pushInfo = async () => {
- const audio = {
- file: DocCast(this.Document.audio)[DocData].url,
- };
- const response = await axios.post('http://localhost:105/recognize/', audio, {
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- this.Document.phoneticTranscription = response.data.transcription;
- };
+ 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.
@@ -387,147 +485,55 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
* Gets the transcript of a youtube video by sending the video url to the backend.
* @returns transcription of youtube recording
*/
- youtubeUpload = async () => {
- const audio = {
- file: this.getYouTubeVideoId(StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)),
- };
- const response = await axios.post('http://localhost:105/youtube/', audio, {
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- return response.data.transcription;
- };
-
- createFlashcardPile(collectionArr: Doc[], gpt: boolean) {
- const newCol = Docs.Create.CarouselDocument(collectionArr, {
- _width: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 250) + 50,
- _height: NumCast(this.layoutDoc['_' + this._props.fieldKey + '_width'], 200) + 50,
- _layout_fitWidth: false,
- _layout_autoHeight: true,
- _xMargin: 5,
- _yMargin: 5,
- });
- newCol.x = this.layoutDoc.x;
- newCol.y = NumCast(this.layoutDoc.y) + 50;
- newCol.type_collection = CollectionViewType.Carousel as string;
- for (let i = 0; i < collectionArr.length; i++) {
- DocCast(collectionArr[i])[DocData].embedContainer = newCol;
- }
-
- if (gpt) {
- this._props.DocumentView?.()._props.addDocument?.(newCol);
- this._props.removeDocument?.(this.Document);
- } else {
- this._props.addDocument?.(newCol);
- this._props.removeDocument?.(this.Document);
- this.Document.embedContainer = newCol;
- }
- }
-
- /**
- * Transfers the content of flashcards into a flashcard pile.
- */
- gptFlashcardPile = async () => {
- const text = await this.askGPT(GPTCallType.STACK);
- const senArr = text?.split('Question: ') ?? [];
- const collectionArr: Doc[] = [];
- for (let i = 1; i < senArr.length; i++) {
- const newDoc = Docs.Create.ComparisonDocument(senArr[i], { _layout_isFlashcard: true, _width: 300, _height: 300 });
-
- if (senArr[i].includes('Keyword: ')) {
- const question = StrCast(senArr![i]).split('Keyword: ');
- const questionTxt = question[0].includes('Answer: ') ? question[0].split('Answer: ')[0] : question[0];
- const answerTxt = question[0].includes('Answer: ') ? question[0].split('Answer: ')[1] : question[1];
- this.fetchImages(question[1]).then(img => {
- const rtfiel = new RichTextField(
- JSON.stringify({
- // this is a RichText json that has the question text placed above a related image
- doc: {
- type: 'doc',
- content: [
- {
- type: 'paragraph',
- attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
- content: [{ type: 'text', text: questionTxt }, img ? { type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: img[Id] } } : {}],
- },
- ],
- },
- selection: { type: 'text', anchor: 2, head: 2 },
- }),
- questionTxt
- );
- newDoc[DocData][this.fieldKey + '_1'] = Docs.Create.TextDocument(questionTxt, { text: rtfiel });
- newDoc[DocData][this.fieldKey + '_0'] = Docs.Create.TextDocument(answerTxt);
- });
- }
-
- collectionArr.push(newDoc);
- }
- this.createFlashcardPile(collectionArr, true);
- };
+ 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): Promise<string | undefined> => {
- const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
- // const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text);
- // const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText;
- this._loading = true;
-
- if (callType == GPTCallType.CHATCARD) {
- if (StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '') {
- this._loading = false;
- return;
- }
- }
- try {
- const res = await gptAPICall(questionText, GPTCallType.FLASHCARD);
- runInAction(() => {
- if (!res) {
- console.error('GPT call failed');
- return;
- }
- if (callType == GPTCallType.CHATCARD) {
- DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
- } else if (callType == GPTCallType.QUIZ) {
- this._frontSide = true;
- this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
- } else if (callType === GPTCallType.FLASHCARD) {
- this._loading = false;
- return res;
- }
- this._loading = false;
- });
- return res;
- } catch (err) {
- console.error('GPT call failed', err);
- }
- this._loading = false;
+ askGPT = async (callType: GPTCallType) => {
+ const questionText = 'Question: ' + this.frontText;
+ const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : '');
+
+ this.loading = true;
+ const res = !this.frontText
+ ? ''
+ : await gptAPICall(queryText, callType).then(
+ action(resp => {
+ switch (resp && callType) {
+ case GPTCallType.CHATCARD:
+ DocCast(this.dataDoc[this.backKey])[DocData].text = resp;
+ break;
+ case GPTCallType.QUIZ:
+ this._renderSide = this.backKey;
+ this._outputValue = resp.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
+ break;
+ case GPTCallType.FLASHCARD:
+ default:
+ }
+ return resp;
+ })
+ );
+ this.loading = false;
+ if (!res) console.error('GPT call failed');
+ return res;
};
layoutWidth = () => NumCast(this.layoutDoc.width, 200);
layoutHeight = () => NumCast(this.layoutDoc.height, 200);
- findImageTags = async () => {
- const c = this.DocumentView?.().ContentDiv?.getElementsByTagName('img');
- if (c?.length === 0) this.askGPT(GPTCallType.CHATCARD);
- if (c) {
- this._loading = true;
- for (const i of c) {
- if (i.className !== 'ProseMirror-separator') this.getImageDesc(i.src);
- }
- this._loading = false;
- }
- };
-
/**
* 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 = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
+ const sentence = this.frontText;
const phon6 = 'huː ɑɹ juː tədeɪ';
const phon4 = 'kamo estas hɔi';
const promptEng =
@@ -553,17 +559,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
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;
- }
+ 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
};
/**
@@ -586,45 +586,37 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
* @param selection
* @returns Image Document
*/
- fetchImages = async (selection: string) => {
+ public static async fetchImages(selection: string) {
try {
const { data } = await axios.get(`${API_URL}?query=${selection}&page=1&per_page=${1}&client_id=Q4zruu6k6lum2kExiGhLNBJIgXDxD6NNj0SRHH_XXU0`);
const imageSnapshot = Docs.Create.ImageDocument(data.results[0].urls.small, {
- _nativeWidth: Doc.NativeWidth(this.layoutDoc),
- _nativeHeight: Doc.NativeHeight(this.layoutDoc),
- x: NumCast(this.layoutDoc.x),
- y: NumCast(this.layoutDoc.y),
onClick: FollowLinkScript(),
_width: 150,
_height: 150,
- title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
+ title: selection,
});
- imageSnapshot.x = this.layoutDoc.x;
- imageSnapshot.y = this.layoutDoc.y;
return imageSnapshot;
} catch (error) {
console.log(error);
}
- };
+ }
getImageDesc = async (u: string) => {
try {
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.props.fieldKey + '_0'])[DocData].text = response;
+ DocCast(this.dataDoc[this.backKey])[DocData].text = response;
} catch (error) {
console.log('Error', error);
}
};
- @action
- flipFlashcard = () => {
- this._frontSide = !this._frontSide;
- };
-
- hoverFlip = (side: boolean) => {
- if (this.revealOp === 'hover') this._frontSide = side;
+ flashcardContextMenu = () => {
+ const appearance = ContextMenu.Instance.findByDescription('Appearance...');
+ const appearanceItems = appearance?.subitems ?? [];
+ appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(GPTCallType.CHATCARD), icon: 'id-card' });
+ !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
};
testForTextFields = (whichSlot: string) => {
@@ -634,8 +626,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim();
const layoutTemplateString =
slotHasText ? FormattedTextBox.LayoutString(whichSlot):
- whichSlot.endsWith('1') ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) :
- altText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore
+ 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)
@@ -643,8 +635,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
// 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_2)
- if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) {
+ // 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
@@ -652,179 +644,167 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
}
return layoutTemplateString;
};
-
childActiveFunc = () => this._childActive;
contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
- render() {
- const clearButton = (which: string) => (
- <Tooltip title={<div className="dash-tooltip">remove</div>}>
- <div
- ref={this._closeRef}
- className={`clear-button ${which}`}
- onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding
- >
- <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="xs" />
- </div>
- </Tooltip>
- );
- const displayDoc = (whichSlot: string) => {
- const whichDoc = DocCast(this.dataDoc[whichSlot]);
- const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
- const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot);
-
- return targetDoc || layoutString ? (
- <>
- <DocumentView
- {...this._props}
- showTags={undefined}
- fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option
- ignoreUsePath={layoutString ? true : undefined}
- renderDepth={this.props.renderDepth + 1}
- LayoutTemplateString={layoutString}
- Document={layoutString ? this.Document : targetDoc}
- containerViewPath={this._props.docViewPath}
- moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
- removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- ScreenToLocalTransform={this.contentScreenToLocalXf}
- isContentActive={this.childActiveFunc}
- isDocumentActive={returnFalse}
- dontSelect={returnTrue}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- styleProvider={this._childActive ? this._props.styleProvider : this.docStyleProvider}
- hideLinkButton
- pointerEvents={this._childActive ? undefined : returnNone}
- />
- {!this.Document._layout_isFlashcard ? clearButton(whichSlot) : null}
- </>
- ) : (
- <div className="placeholder">
- <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" />
- </div>
- );
- };
- const displayBox = (which: string, index: number, cover: number) => (
+
+ clearButton = (which: string) => (
+ <Tooltip title={<div className="dash-tooltip">remove</div>}>
<div
- className={`${index === 0 ? 'before' : 'after'}Box-cont`}
- key={which}
- style={{ width: this._props.PanelWidth() }}
- onPointerDown={e => {
- this.registerSliding(e, cover);
- this.Document._layout_isFlashcard && this.activateContent();
- }}
- ref={ele => this.createDropTarget(ele, which, index)}>
- {!this._isEmpty ? displayDoc(which) : null}
+ ref={this._closeRef}
+ className={`clear-button ${which}`}
+ onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding
+ >
+ <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="xs" />
+ </div>
+ </Tooltip>
+ );
+ displayDoc = (whichSlot: string) => {
+ const whichDoc = DocCast(this.dataDoc[whichSlot]);
+ const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
+ const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot);
+
+ return targetDoc || layoutString ? (
+ <>
+ <DocumentView
+ {...this._props}
+ Document={layoutString ? this.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={undefined} // 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}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ styleProvider={this._childActive ? this._props.styleProvider : this.docStyleProvider}
+ hideLinkButton
+ pointerEvents={this._childActive ? undefined : returnNone}
+ />
+ {!this.isFlashcard ? this.clearButton(whichSlot) : null}
+ </>
+ ) : (
+ <div className="placeholder">
+ <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" />
</div>
);
+ };
- if (this.Document._layout_isFlashcard) {
- const side = this._frontSide ? 1 : 0;
- const dataSplit = StrCast(this.dataDoc.data).includes('Keyword: ') ? StrCast(this.dataDoc.data).split('Keyword: ') : StrCast(this.dataDoc.data).split('Answer: ');
- const textCreator = (which: number, title: string, text: string) => {
- const newDoc = Docs.Create.TextDocument(text, {
- title, //
- _layout_autoHeight: true,
- _layout_centered: true,
- text_align: 'center',
- _layout_fitWidth: true,
- });
- this.addDoc(newDoc, this.fieldKey + '_' + which);
- return newDoc;
- };
-
- // add text box to each side when comparison box is first created
- if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
- textCreator(0, 'answer', dataSplit[1]);
- }
-
- if (!this.dataDoc[this.fieldKey + '_1'] && !this._isEmpty) {
- const question = textCreator(1, 'question', dataSplit[0] || 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards');
- Doc.SelectOnLoad = dataSplit[0] ? undefined : question;
- }
-
- if (DocCast(this.Document.embedContainer).practiceMode === practiceMode.QUIZ) {
- const text = StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
- return (
- <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`}>
- <p style={{ color: 'white', padding: 10 }}>{text}</p>
- <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p>
- <div className="input-box">
- <textarea
- value={this._frontSide ? this._outputValue : this._inputValue}
- onChange={this.handleInputChange}
- onScroll={e => {
- e.stopPropagation();
- e.preventDefault();
- }}
- placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
- readOnly={this._frontSide}></textarea>
-
- {this._loading ? (
- <div className="loading-spinner">
- <ReactLoading type="spin" height={30} width={30} color={'blue'} />
- </div>
- ) : null}
- </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._frontSide ? this.handleRenderClick : this.handleRenderGPTClick}>
- {this._frontSide ? 'Redo the Question' : 'Submit'}
- </button>
- </div>
- </div>
+ displayBox = (which: string, cover: number) => (
+ <div className={`${which === this.frontKey ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which)}>
+ {this.displayDoc(which)}
+ </div>
+ );
+
+ /* renders front(qustion) and back(answer) at the same time, then on user input replaces the answer with a GPT analysis of the answer */
+ renderAsQuiz = (text: string) => (
+ <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`}>
+ <p style={{ color: 'white', padding: 10 }}>{text}</p>
+ <p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p>
+ <div className="input-box">
+ <textarea
+ value={this._renderSide === this.backKey ? this._outputValue : this._inputValue}
+ onChange={action(e => {
+ this._inputValue = e.target.value;
+ })}
+ placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
+ readOnly={this._renderSide === this.backKey}
+ />
+ {!this.loading ? null : (
+ <div className="loading-spinner">
+ <ReactLoading type="spin" height={30} width={30} color="blue" />
</div>
- );
- }
-
- // render a normal flashcard when not a QuizCard
- return (
- <div
- className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
- style={{ display: 'flex', flexDirection: 'column' }}
- onMouseEnter={() => this.hoverFlip(true)}
- onMouseLeave={() => this.hoverFlip(false)}>
- {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
- {this._loading ? (
- <div className="loading-spinner">
- <ReactLoading type="spin" height={30} width={30} color={'blue'} />
- </div>
- ) : null}
- {this.flashcardMenu}
- </div>
- );
- }
- // render a comparison box that compares items side by side
- return (
- <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
- {displayBox(`${this.fieldKey}_2`, 1, this._props.PanelWidth() - 3)}
- <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
- {displayBox(`${this.fieldKey}_1`, 0, 0)}
+ )}
+ </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>
</div>
+ </div>
+ </div>
+ );
+
+ // if flashcard is rendered that has no data, then add some placeholders for question and answer
+ // addPlaceholdersForEmptyFlashcard = () => {
+ // if (this.dataDoc.data) {
+ // if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) ComparisonBox.createFlashcard(StrCast(this.dataDoc.data), this.frontKey, this.backKey, this.Document);
+ // }
+ // };
+
+ // render a button that flips between front and back
+ renderAsFlip = () => (
+ <div
+ style={{ display: 'flex', pointerEvents: this.revealOpHover && this._props.isContentActive() ? 'unset' : undefined }} //
+ onMouseEnter={() => this.revealOpHover && this.animateFlipping(this.backKey)}
+ onMouseLeave={() => this.revealOpHover && this.animateFlipping(this.frontKey)}>
+ <div style={{ position: 'absolute', width: '100%', height: '100%', transition: this._animating === '0' ? undefined : this._animating, opacity: this._animating === '0' ? 1 : 0 }}>
+ {this.displayBox(this._renderSide === this.backKey ? this.frontKey : this.backKey, 0)}
+ </div>
+ <div style={{ position: 'absolute', width: '100%', height: '100%', transition: this._animating === '0' ? undefined : this._animating, opacity: this._animating === '0' ? 0 : 1 }}>{this.displayBox(this._renderSide, 0)}</div>
+ {this.flashcardMenu}
+ </div>
+ );
+
+ // render a slider that reveals front and back as slider is dragged horizonally
+ renderAsBeforeAfter = () => (
+ <div
+ className="comparisonBox-slide"
+ style={{ display: 'flex', pointerEvents: this.revealOpHover && this._props.isContentActive() ? 'unset' : undefined }}
+ onMouseEnter={() => this.revealOpHover && this.animateSliding(0)}
+ onMouseLeave={() => this.revealOpHover && this.animateSliding(this._props.PanelWidth() - 3)}>
+ {this.displayBox(this.backKey, this._props.PanelWidth() - 3)}
+ <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
+ {this.displayBox(this.frontKey, 0)}
+ </div>
- <div
- className="slide-bar"
- style={{
- left: `calc(${this.clipWidth + '%'} - 0.5px)`,
- cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined,
- }}
- onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */
- >
- <div className="slide-handle" />
- </div>
+ <div
+ className="slide-bar"
+ style={{
+ left: `calc(${this.clipWidth + '%'} - 0.5px)`,
+ cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined,
+ }}
+ onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */
+ >
+ <div className="slide-handle" />
+ </div>
+ </div>
+ );
+
+ render() {
+ const renderMode = new Map<flashcardRevealOp, () => JSX.Element>([
+ [flashcardRevealOp.FLIP, this.renderAsFlip],
+ [flashcardRevealOp.SLIDE, this.renderAsBeforeAfter]]); // prettier-ignore
+ return this.isQuizMode ? (
+ this.renderAsQuiz(this.frontText)
+ ) : (
+ <div className="comparisonBox" style={{ pointerEvents: this._props.isContentActive() && !this.Document[Animation] ? 'unset' : undefined }} onContextMenu={this.flashcardContextMenu}>
+ {renderMode.get(this.revealOp)?.() ?? null}
+ {this.loading ? (
+ <div className="loading-spinner">
+ <ReactLoading type="spin" height={30} width={30} color="blue" />
+ </div>
+ ) : null}
</div>
);
}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 896048ab3..7925410e2 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -73,7 +73,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return <div className="dataVizBox-annotationLayer" style={{ height: this._props.PanelHeight(), width: this._props.PanelWidth() }} ref={this._annotationLayer} />;
}
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
setupMoveUpEvents(
this,
e,
@@ -323,7 +323,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
() => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar')
);
};
- getView = async (doc: Doc, options: FocusViewOptions) => {
+ getView = (doc: Doc, options: FocusViewOptions) => {
if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
options.didMove = true;
this.toggleSidebar();
@@ -453,7 +453,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@action
onPointerDown = (e: React.PointerEvent): void => {
if ((this.Document._freeform_scale || 1) !== 1) return;
- if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
this._props.select(false);
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this._marqueeing = [e.clientX, e.clientY];
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
index 6c649bde3..7c601185e 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
@@ -231,7 +231,8 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
this._disposers.lightbox = reaction(
() => LightboxView.LightboxDoc(),
doc => {
- doc ? this._shouldDisplay && this.closeMenu() : !this._shouldDisplay && this.openMenu();
+ // NOTE: bcz; commented this out because the doc creator would appear everytime I close out of the lightbox
+ // doc ? this._shouldDisplay && this.closeMenu() : !this._shouldDisplay && this.openMenu();
}
);
//this._disposers.fields = reaction(() => this._dataViz?.axes, cols => this._selectedCols = cols?.map(col => { return {title: col, type: '', desc: ''}}))
@@ -244,7 +245,7 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
}
updateIcons = (docs: Doc[]) => {
- console.log('called')
+ console.log('called');
docs.map(this.getIcon);
};
@@ -415,7 +416,7 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
const height = bottom - top;
const width = right - left;
const doc = !field.title.includes('$$')
- ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, _text_fontSize: `${height / 2}` })
+ ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, text_fontSize: `${height / 2}` })
: Docs.Create.ImageDocument('', { _height: height, _width: width, title: field.title.replace(/\$\$/g, ''), x: left, y: top });
return doc;
});
@@ -918,17 +919,17 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
@action setExpandedView = (info: { icon: ImageField; doc: Doc } | undefined) => {
if (info) {
const doc = info.doc;
- const wrapper: Doc = Docs.Create.FreeformDocument([info.doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''});
- const newInfo = {icon: new ImageField(''), doc: wrapper}
+ const wrapper: Doc = Docs.Create.FreeformDocument([info.doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: '' });
+ const newInfo = { icon: new ImageField(''), doc: wrapper };
this._expandedPreview = newInfo;
} else {
this._expandedPreview = info;
}
};
- get editingWindow(){
+ get editingWindow() {
const doc = this._expandedPreview?.doc ?? new Doc();
- const rendered =
+ const rendered = (
<div className="docCreatorMenu-expanded-template-preview">
<CollectionFreeFormView
Document={this._expandedPreview!.doc}
@@ -944,7 +945,7 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
removeDocument={returnFalse}
PanelWidth={() => this._menuDimensions.width - 10}
PanelHeight={() => this._menuDimensions.height - 60}
- ScreenToLocalTransform={() => new Transform(-this._pageX,-this._pageY, 1)}
+ ScreenToLocalTransform={() => new Transform(-this._pageX, -this._pageY, 1)}
renderDepth={5}
whenChildContentsActiveChanged={emptyFunction}
focus={emptyFunction}
@@ -960,14 +961,21 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
yPadding={0}
/>
</div>
-
+ );
return (
<div className="docCreatorMenu-expanded-template-preview">
- <div className="top-panel"/>
+ <div className="top-panel" />
{rendered}
<div className="right-buttons-panel">
- <button className="docCreatorMenu-menu-button section-reveal-options top-right" onPointerDown={e => this.setUpButtonClick(e, () => {this._expandedPreview && this.updateIcons(this._suggestedTemplates.slice()); this.setExpandedView(undefined)})}>
+ <button
+ className="docCreatorMenu-menu-button section-reveal-options top-right"
+ onPointerDown={e =>
+ this.setUpButtonClick(e, () => {
+ this._expandedPreview && this.updateIcons(this._suggestedTemplates.slice());
+ this.setExpandedView(undefined);
+ })
+ }>
<FontAwesomeIcon icon="minimize" />
</button>
<button className="docCreatorMenu-menu-button section-reveal-options top-right-lower" onPointerDown={e => this.setUpButtonClick(e, () => this._expandedPreview && this._templateDocs.push(this._expandedPreview.doc))}>
@@ -975,7 +983,6 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
</button>
</div>
</div>
-
);
}
@@ -2259,11 +2266,11 @@ export class FieldUtils {
title: title,
x: coord.x,
y: coord.y,
- _text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`,
+ text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`,
backgroundColor: opts.backgroundColor ?? '',
text_fontColor: opts.color,
contentBold: opts.fontBold,
- textTransform: opts.fontTransform,
+ text_transform: opts.fontTransform,
color: opts.color,
_layout_borderRounding: `${opts.cornerRounding ?? 0}px`,
borderColor: opts.borderColor,
@@ -2305,7 +2312,7 @@ export class FieldUtils {
public static CarouselField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, fields: Doc[]) => {
const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight);
- const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, _text_fontSize: `${height / 2}` });
+ const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, text_fontSize: `${height / 2}` });
return doc;
};
@@ -2358,4 +2365,3 @@ export class FieldUtils {
// }]
// };
// }
-
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 9aa000ba7..aab8a183a 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
import { computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index f7aba7542..30f9e6363 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -16,12 +16,11 @@ import { List } from '../../../fields/List';
import { PrefetchProxy } from '../../../fields/Proxy';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, ImageCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { AudioField } from '../../../fields/URLField';
import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
import { AudioAnnoState } from '../../../server/SharedMediaTypes';
import { DocServer } from '../../DocServer';
-import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
@@ -381,7 +380,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
};
onPointerMove = (e: PointerEvent): void => {
- if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
+ if (e.buttons !== 1 || Doc.ActiveTool === InkTool.Ink) return;
if (!ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
this.cleanupPointerEvents();
@@ -492,21 +491,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
input.click();
};
- askGPT = async (): Promise<string | undefined> => {
- const queryText = RTFCast(DocCast(this.dataDoc[this.props.fieldKey + '_1']).text)?.Text;
- try {
- const res = await gptAPICall('Question: ' + StrCast(queryText), GPTCallType.CHATCARD);
- if (!res) {
- console.error('GPT call failed');
- return;
- }
- DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
- console.log(res);
- } catch (err) {
- console.error('GPT call failed', err);
- }
- };
-
onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
if (this._props.dontSelect?.()) return;
if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) {
@@ -569,22 +553,10 @@ 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' });
- if (this.Document._layout_isFlashcard) {
- appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(), icon: 'id-card' });
- }
!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' });
- // creates menu for the user to select how to reveal the flashcards
- // if (this.Document._layout_isFlashcard) {
- // const revealOptions = cm.findByDescription('Reveal Options');
- // const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
- // revealItems.push({ description: 'Hover', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'; }, icon: 'hand-point-up' }); // prettier-ignore
- // revealItems.push({ description: 'Flip', event: () => { this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'; }, icon: 'rotate' }); // prettier-ignore
- // !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
- // }
-
if (this._props.bringToFront) {
const zorders = cm.findByDescription('ZOrder...');
const zorderItems = zorders?.subitems ?? [];
@@ -908,7 +880,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
background: this.backgroundBoxColor,
opacity: this.opacity,
cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
- color: StrCast(this.layoutDoc.color, 'inherit'),
+ color: StrCast(this.Document._color, 'inherit'),
fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'),
fontSize: Cast(this.Document._text_fontSize, 'string', null),
transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
@@ -1009,12 +981,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
type: SpringType.GENTLE,
...springMappings.gentle,
};
- switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
- case PresEffect.Expand: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Expand} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
- case PresEffect.Flip: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Flip} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
- case PresEffect.Rotate: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Rotate} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
- case PresEffect.Bounce: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Bounce} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
- case PresEffect.Roll: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Roll} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
+ const presEffect = StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect));
+ switch (presEffect) {
+ case PresEffect.Expand: case PresEffect.Flip: case PresEffect.Rotate: case PresEffect.Bounce:
+ case PresEffect.Roll: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={presEffect} springSettings={timingConfig}>{renderDoc}</SpringAnimation>
// case PresEffect.Fade: return <SlideEffect doc={root} dir={dir} presEffect={PresEffect.Fade} tension={timingConfig.stiffness} friction={timingConfig.damping} mass={timingConfig.mass}>{renderDoc}</SlideEffect>
case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>
// keep as preset, doesn't really make sense with spring config
@@ -1590,55 +1560,45 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
} else func();
}
}
-
-export function ActiveFillColor(): string {
- const dv = DocumentView.Selected().lastElement() ?.Document._layout_isSvg ? DocumentView.Selected().lastElement() : undefined;
- return StrCast(dv?.Document.fillColor, StrCast(ActiveInkPen()?.activeFillColor, ""));
-} // prettier-ignore
-export function ActiveInkPen(): Doc { return Doc.UserDoc(); } // prettier-ignore
-export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, 'black'); } // prettier-ignore
-export function ActiveIsInkMask(): boolean { return BoolCast(ActiveInkPen()?.activeIsInkMask, false); } // prettier-ignore
-export function ActiveInkHideTextLabels(): boolean { return BoolCast(ActiveInkPen().activeInkHideTextLabels, false); } // prettier-ignore
-export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ''); } // prettier-ignore
-export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ''); } // prettier-ignore
-export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } // prettier-ignore
-export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore
-export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore
-export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore
-export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth ?? 25); } // prettier-ignore
-
+export function ActiveHideTextLabels(): boolean { return BoolCast(Doc.UserDoc().activeHideTextLabels, false); } // prettier-ignore
+export function ActiveIsInkMask(): boolean { return BoolCast(Doc.UserDoc()?.activeIsInkMask, false); } // prettier-ignore
+export function ActiveEraserWidth(): number { return Number(Doc.UserDoc()?.activeEraserWidth ?? 25); } // prettier-ignore
+
+export function ActiveInkFillColor(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Fill`]); } // prettier-ignore
+export function ActiveInkColor(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Color`], 'black'); } // prettier-ignore
+export function ActiveInkArrowStart(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowStart`], ''); } // prettier-ignore
+export function ActiveInkArrowEnd(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowEnd`], ''); } // prettier-ignore
+export function ActiveInkArrowScale(): number { return NumCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}ArrowScale`], 1); } // prettier-ignore
+export function ActiveInkDash(): string { return StrCast(Doc.UserDoc()?.[`active${Doc.ActiveInk}Dash`], '0'); } // prettier-ignore
+export function ActiveInkWidth(): number { return Number(Doc.UserDoc()?.[`active${Doc.ActiveInk}Width`]); } // prettier-ignore
+export function ActiveInkBezierApprox(): string { return StrCast(Doc.UserDoc()[`active${Doc.ActiveInk}Bezier`]); } // prettier-ignore
+
+export function SetActiveIsInkMask(value: boolean) { Doc.UserDoc() && (Doc.UserDoc().activeIsInkMask = value); } // prettier-ignore
+export function SetactiveHideTextLabels(value: boolean) { Doc.UserDoc() && (Doc.UserDoc().activeHideTextLabels = value); } // prettier-ignore
+export function SetEraserWidth(width: number): void { Doc.UserDoc() && (Doc.UserDoc().activeEraserWidth = width); } // prettier-ignore
export function SetActiveInkWidth(width: string): void {
- !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width);
+ !isNaN(parseInt(width)) && Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Width`] = width);
}
-export function SetActiveBezierApprox(bezier: string): void {
- ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier);
+export function SetActiveInkBezierApprox(bezier: string): void {
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Bezier`] = isNaN(parseInt(bezier)) ? '' : bezier);
}
export function SetActiveInkColor(value: string) {
- ActiveInkPen() && (ActiveInkPen().activeInkColor = value);
-}
-export function SetActiveIsInkMask(value: boolean) {
- ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value);
-}
-export function SetActiveInkHideTextLabels(value: boolean) {
- ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value);
-}
-export function SetActiveFillColor(value: string) {
- ActiveInkPen() && (ActiveInkPen().activeFillColor = value);
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Color`] = value);
}
-export function SetActiveArrowStart(value: string) {
- ActiveInkPen() && (ActiveInkPen().activeArrowStart = value);
+export function SetActiveInkFillColor(value: string) {
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}Fill`] = value);
}
-export function SetActiveArrowEnd(value: string) {
- ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value);
+export function SetActiveInkArrowStart(value: string) {
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowStart`] = value);
}
-export function SetActiveArrowScale(value: number) {
- ActiveInkPen() && (ActiveInkPen().activeArrowScale = value);
+export function SetActiveInkArrowEnd(value: string) {
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowEnd`] = value);
}
-export function SetActiveDash(dash: string): void {
- !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash);
+export function SetActiveInkArrowScale(value: number) {
+ Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}ArrowScale`] = value);
}
-export function SetEraserWidth(width: number): void {
- ActiveInkPen() && (ActiveInkPen().eraserWidth = width);
+export function SetActiveInkDash(dash: string): void {
+ !isNaN(parseInt(dash)) && Doc.UserDoc() && (Doc.UserDoc()[`active${Doc.ActiveInk}`] = dash);
}
// eslint-disable-next-line prefer-arrow-callback
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index fefe25764..290c90d6e 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -122,7 +122,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
width: 'fit-content', // `${100 / scale}%`,
height: `${100 / scale}%`,
pointerEvents: !this._props.isSelected() ? 'none' : undefined,
- fontSize: StrCast(this.layoutDoc._text_fontSize),
+ fontSize: StrCast(this.Document._text_fontSize),
}}
onKeyDown={e => e.stopPropagation()}>
<EquationEditor ref={this._ref} value={StrCast(this.dataDoc.text, 'x')} spaceBehavesLikeTab onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" />
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index 2db285910..2405889cf 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -10,6 +10,13 @@
height: 3px !important;
}
}
+.fonticonbox {
+ margin: auto;
+ width: 100%;
+ .formLabel {
+ height: 5px;
+ }
+}
.menuButton {
height: 100%;
display: flex;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index feaf84b7b..8c138c2ee 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -4,7 +4,7 @@ import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps
import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { ClientUtils, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
+import { ClientUtils, DashColor, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { emptyFunction } from '../../../../Utils';
@@ -21,6 +21,8 @@ import { FieldView, FieldViewProps } from '../FieldView';
import { OpenWhere } from '../OpenWhere';
import './FontIconBox.scss';
import TrailsIcon from './TrailsIcon';
+import { InkTool } from '../../../../fields/InkField';
+import { ScriptField } from '../../../../fields/ScriptField';
export enum ButtonType {
TextButton = 'textBtn',
@@ -126,7 +128,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
background={SnappingManager.userBackgroundColor}
numberDropdownType={type}
showPlusMinus={false}
- tooltip={this.label}
+ formLabel={(StrCast(this.Document.title).startsWith(' ') ? '\u00A0' : '') + StrCast(this.Document.title)}
+ tooltip={StrCast(this.Document.toolTip, this.label)}
type={Type.PRIM}
min={NumCast(this.dataDoc.numBtnMin, 0)}
max={NumCast(this.dataDoc.numBtnMax, 100)}
@@ -149,64 +152,79 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
};
/**
+ * Displays custom dropdown menu for fonts -- this is a HACK -- fix for generality, don't copy
+ */
+ handleFontDropdown = (script: () => string, buttonList: string[]) => {
+ // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
+ return {
+ buttonList,
+ jsx: undefined,
+ selectedVal: script(),
+ getStyle: (val: string) => ({ fontFamily: val }),
+ };
+ };
+ /**
+ * Displays custom dropdown menu for view selection -- this is a HACK -- fix for generality, don't copy
+ */
+ handleViewDropdown = (script: ScriptField, buttonList: string[]) => {
+ const selected = Array.from(script?.script.run({ _readOnly_: true }).result as Doc[]);
+ const noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Card, CollectionViewType.Carousel3D, CollectionViewType.Carousel, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
+ return selected.length === 1 && selected[0].type === DocumentType.COL
+ ? {
+ buttonList: buttonList.filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value as CollectionViewType)),
+ getStyle: undefined,
+ selectedVal: StrCast(selected[0]._type_collection),
+ }
+ : {
+ jsx: selected.length ? (
+ <Popup
+ icon={<FontAwesomeIcon size="1x" icon={selected.length > 1 ? 'caret-down' : (Doc.toIcon(selected.lastElement()) as IconProp)} />}
+ text={selected.length === 1 ? ClientUtils.cleanDocumentType(StrCast(selected[0].type) as DocumentType) : selected.length + ' selected'}
+ type={Type.TERT}
+ color={SnappingManager.userColor}
+ background={SnappingManager.userVariantColor}
+ popup={<SelectedDocView selectedDocs={selected} />}
+ fillWidth
+ />
+ ) : (
+ <Button
+ text={`${Doc.ActiveTool === InkTool.None ? 'Text box' : Doc.ActiveInk} defaults`} //
+ type={Type.TERT}
+ color={SnappingManager.userColor}
+ background={SnappingManager.userVariantColor}
+ fillWidth
+ inactive
+ />
+ ),
+ };
+ };
+
+ /**
* Dropdown list
*/
@computed get dropdownListButton() {
const script = ScriptCast(this.Document.script);
-
- let noviceList: string[] = [];
- let text: string | undefined;
- let getStyle: (val: string) => { [key: string]: string } = () => ({});
- let icon: IconProp = 'caret-down';
- const isViewDropdown = script?.script.originalScript.startsWith('{ return setView');
- if (isViewDropdown) {
- const selected = Array.from(script?.script.run({ _readOnly_: true }).result as Doc[]);
- // const selected = DocumentView.SelectedDocs();
- if (selected.lastElement()) {
- if (StrCast(selected.lastElement().type) === DocumentType.COL) {
- text = StrCast(selected.lastElement()._type_collection);
- } else {
- if (selected.length > 1) {
- text = selected.length + ' selected';
- } else {
- text = ClientUtils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType, '' as CollectionViewType);
- icon = Doc.toIcon(selected.lastElement());
- }
- return (
- <Popup
- icon={<FontAwesomeIcon size="1x" icon={icon} />}
- text={text}
- type={Type.TERT}
- color={SnappingManager.userColor}
- background={SnappingManager.userVariantColor}
- popup={<SelectedDocView selectedDocs={selected} />}
- fillWidth
- />
- );
- }
- } else {
- return <Button text="None Selected" type={Type.TERT} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} fillWidth inactive />;
- }
- noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Carousel3D, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
- } else {
- text = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
- // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
- if (this.Document.title === 'Font') getStyle = (val: string) => ({ fontFamily: val }); // bcz: major hack to style the font dropdown items --- needs to become part of the dropdown's metadata
- }
+ const selectedFunc = () => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
+ const { buttonList, selectedVal, getStyle, jsx } = (() => {
+ switch (this.Document.title) {
+ case 'Font': return this.handleFontDropdown(selectedFunc, this.buttonList);
+ case 'Perspective': return this.handleViewDropdown(script, this.buttonList);
+ default: return { buttonList: this.buttonList, selectedVal: selectedFunc(), jsx: undefined, getStyle: undefined };
+ } // prettier-ignore
+ })();
+ if (jsx) return jsx;
// Get items to place into the list
- const list: IListItemProps[] = this.buttonList
- .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value))
- .map(value => ({
- text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title),
- val: value,
- style: getStyle(value),
- // shortcut: '#',
- }));
+ const list: IListItemProps[] = buttonList.map(value => ({
+ text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title),
+ val: value,
+ style: getStyle?.(value),
+ // shortcut: '#',
+ }));
return (
<Dropdown
- selectedVal={text}
+ selectedVal={selectedVal}
setSelectedVal={undoable(value => script.script.run({ this: this.Document, value }), `dropdown select ${this.label}`)}
color={SnappingManager.userColor}
background={SnappingManager.userVariantColor}
@@ -215,7 +233,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
dropdownType={DropdownType.SELECT}
onItemDown={this.dropdownItemDown}
items={list}
- tooltip={this.label}
+ tooltip={StrCast(this.Document.toolTip, this.label)}
fillWidth
/>
);
@@ -235,49 +253,53 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
const tooltip: string = StrCast(this.Document.toolTip);
return (
- <ColorPicker
- setSelectedColor={value => {
- if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
- this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
- }}
- setFinalColor={value => {
- this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
- this.colorBatch?.end();
- this.colorBatch = undefined;
- }}
- defaultPickerType="Classic"
- selectedColor={curColor}
- type={Type.PRIM}
- color={color}
- background={SnappingManager.userBackgroundColor}
- icon={this.Icon(color) ?? undefined}
- tooltip={tooltip}
- label={this.label}
- />
+ <div
+ onPointerDown={e => {
+ e.stopPropagation();
+ }}>
+ <ColorPicker
+ setSelectedColor={value => {
+ if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
+ this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+ }}
+ setFinalColor={value => {
+ this.colorScript?.script.run({ this: this.Document, value: value, _readOnly_: false });
+ this.colorBatch?.end();
+ this.colorBatch = undefined;
+ }}
+ defaultPickerType="Classic"
+ selectedColor={curColor}
+ type={Type.PRIM}
+ color={color}
+ background={SnappingManager.userBackgroundColor}
+ icon={this.Icon(color) ?? undefined}
+ tooltip={tooltip}
+ label={this.label}
+ />
+ </div>
);
}
@computed get multiToggleButton() {
- // Determine the type of toggle button
- const tooltip: string = StrCast(this.Document.toolTip);
+ const tooltip = StrCast(this.Document.toolTip);
const script = ScriptCast(this.Document.onClick)?.script;
const toggleStatus = script?.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean;
- // Colors
+
const color = 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 items = DocListCast(this.dataDoc.data);
const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType));
+
return (
<MultiToggle
- tooltip={`Toggle ${tooltip}`}
+ tooltip={`Click to Toggle ${tooltip} or select new option`}
type={Type.PRIM}
color={color}
- background={background === SnappingManager.userBackgroundColor ? undefined : background}
+ background={undefined}
multiSelect={true}
onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))}
isToggle={false}
toggleStatus={toggleStatus}
- label={this.label}
+ 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} />,
tooltip: StrCast(item.toolTip),
@@ -290,7 +312,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, itemDoc, _readOnly_: false }));
+ itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false }));
}}
/>
);
@@ -308,17 +330,19 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
const toggleStatus = (script?.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean) ?? false;
// Colors
const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
- // const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
+ // bcz: ink shapes are tri-state - off, one-shot, and on. Need to update Toggle buttons to allow this and update currentUserUtils to set the tri-state on the Doc
+ // in the meantime, if the button matches a tool type that is not locked, we want to set the background color to something distinct.
+ const inkShapeHack = ((this.Document.toolType && this.Document.toolType === SnappingManager.InkShape) || this.Document.toolType === Doc.ActiveTool) && !SnappingManager.KeepGestureMode;
return (
<Toggle
tooltip={`Toggle ${tooltip}`}
toggleType={ToggleType.BUTTON}
- type={Type.PRIM}
+ type={inkShapeHack ? Type.TERT : Type.PRIM}
toggleStatus={toggleStatus}
text={buttonText}
color={color}
- // background={SnappingManager.userBackgroundColor}
+ background={inkShapeHack ? DashColor(SnappingManager.userBackgroundColor).darken(0.05).toString() : undefined}
icon={this.Icon(color)!}
label={this.label}
onPointerDown={e =>
@@ -392,7 +416,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
render() {
return (
- <div style={{ margin: 'auto', width: '100%' }} onContextMenu={this.specificContextMenu}>
+ <div className="fonticonbox" onContextMenu={this.specificContextMenu}>
{this.renderButton()}
</div>
);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index f98ff6d6e..4d35068ee 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -471,13 +471,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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() &&
- ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)
- ) {
+ } 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,
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 3daacc9bb..40c687b7e 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -114,7 +114,7 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (key) target[key] = script.originalScript;
return false;
}
- field === undefined && (field = res.result instanceof Array ? new List<FieldType>(res.result) : (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;
diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss
index ca4b3d467..889cdc0ca 100644
--- a/src/client/views/nodes/LabelBox.scss
+++ b/src/client/views/nodes/LabelBox.scss
@@ -13,7 +13,6 @@
height: 100%;
border-radius: inherit;
//letter-spacing: 2px; // bcz: doesn't work with LabelBigText
- text-transform: uppercase;
overflow: hidden;
display: inline-block;
margin: auto;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 94a9541f2..dcf9e1fed 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -1,19 +1,22 @@
import { Property } from 'csstype';
-import { action, computed, makeObservable } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import * as textfit from 'textfit';
-import { Field, FieldType } from '../../../fields/Doc';
-import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { Doc, Field } from '../../../fields/Doc';
+import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
+import { undoable } from '../../util/UndoManager';
import { ViewBoxBaseComponent } from '../DocComponent';
import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { FieldView, FieldViewProps } from './FieldView';
import './LabelBox.scss';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { RichTextMenu } from './formattedText/RichTextMenu';
@observer
export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -22,7 +25,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
private dropDisposer?: DragManager.DragDropDisposer;
private _timeout: NodeJS.Timeout | undefined;
- _divRef: HTMLDivElement | null = null;
+ private _divRef: HTMLDivElement | null = null;
+ private _reaction: IReactionDisposer | undefined;
constructor(props: FieldViewProps) {
super(props);
@@ -36,21 +40,29 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
- @computed get Title() {
- return Field.toString(this.dataDoc[this.fieldKey] as FieldType) || StrCast(this.Document.title);
- }
-
- @computed get backgroundColor() {
- return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string;
- }
-
componentDidMount() {
this._props.setContentViewBox?.(this);
+ this._reaction = reaction(
+ () => this.Title,
+ () => document.activeElement !== this._divRef && this._forceRerender++
+ );
}
componentWillUnMount() {
this._timeout && clearTimeout(this._timeout);
+ this.setText(this._divRef?.innerText ?? '');
+ this._reaction?.();
}
+ @observable _forceRerender = 0;
+
+ @computed get Title() { return Field.toString(this.dataDoc[this.fieldKey]); } // prettier-ignore
+ @computed get backgroundColor() { return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; } // prettier-ignore
+ @computed get boxShadow() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string; } // prettier-ignore
+
+ setText = undoable((text: string) => {
+ this.dataDoc[this.fieldKey] = text;
+ }, 'set label text');
+
drop = (/* e: Event, de: DragManager.DropEvent */) => {
return false;
};
@@ -82,10 +94,11 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
const textfitParams = {
minFontSize: NumCast(this.layoutDoc._label_minFontSize, 1),
maxFontSize: NumCast(this.layoutDoc._label_maxFontSize, 100),
- multiLine: BoolCast(this.layoutDoc._singleLine, true) ? false : true,
- alignHoriz: true,
+ multiLine: r?.textContent?.includes('\n') ? true : false,
+ // hack because tetFit doesn't support align 'right', but we need mobx to invalidate, so treat null as false and set to right inline
+ alignHoriz: StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'center' ? true : StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'right' ? (null as unknown as boolean) : false,
alignVert: true,
- detectMultiLine: true,
+ detectMultiLine: false,
};
if (r) {
if (!r.offsetHeight || !r.offsetWidth) {
@@ -94,65 +107,140 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._timeout = setTimeout(() => this.fitTextToBox(r));
return textfitParams;
}
+ r.style.whiteSpace = ''; // textfit sets to nowrap if not multiline, but doesn't reeset if it becomes multiline
+ r.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']); // textfit doesn't reset textAlign if it has been set to center, so we just set it to what we want
+ r.firstChild instanceof HTMLElement && (r.firstChild.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']));
textfit(r, textfitParams);
}
return textfitParams;
};
+ resetCursor = (cranchor?: number) => {
+ if (this._divRef && (cranchor || this._divRef === document.activeElement)) {
+ const range = document.createRange();
+ const anchor = cranchor ?? this._divRef.childNodes.length;
+ const container = cranchor === undefined ? this._divRef : (this._divRef.firstChild?.firstChild ?? this._divRef);
+ range.setStart(container, anchor);
+ range.setEnd(container, anchor);
+ const sel = window.getSelection();
+ sel?.removeAllRanges();
+ sel?.addRange(range);
+ }
+ };
+
+ beforeInput = action((event: InputEvent) => {
+ const spanChild = this._divRef?.firstChild?.firstChild;
+ if (spanChild?.nodeName === '#text' && ['insertLineBreak', 'insertParagraph'].includes(event.inputType)) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const selection = document.getSelection();
+ if (selection && document.activeElement === event.target) {
+ const text = spanChild.textContent ?? '';
+ const cranchor = selection.anchorNode === this._divRef ? (selection.anchorOffset ? text.length : 0) : selection.anchorOffset;
+ const addReturnHack = text.length <= cranchor && text[text.length - 1] !== '\n' ? '\n\n' : '\n'; // not sure why, but need to add a second carriage return if typing enter at the end of the text
+ const splitText = text.substring(0, cranchor) + addReturnHack + text.substring(cranchor);
+ spanChild.textContent = splitText;
+ this.resetCursor(cranchor + addReturnHack.length);
+ }
+ // const span = document.createElement('span');
+ // span.innerHTML = '&#8203;';
+ // this._divRef!.append(span);
+ }
+ });
+ // .labelBox-mainButton > div > span:nth-child(2) {
+
+ /**
+ * When an IconButton is clicked, it will receive focus. However, we don't want that since we want or need that since we really want
+ * to maintain focus in the label's editing div (and cursor position). so this relies on IconButton's having a tabindex set to -1 so that
+ * we can march up the tree from the 'relatedTarget' to determine if the loss of focus was caused by a fonticonbox. If it is, we then
+ * restore focus
+ * @param e focusout event on the editing div
+ */
+ keepFocus = (e: FocusEvent) => {
+ if (e.relatedTarget instanceof HTMLElement && e.relatedTarget.tabIndex === -1) {
+ for (let ele: HTMLElement | null = e.relatedTarget; ele; ele = (ele as HTMLElement)?.parentElement) {
+ if ((ele as HTMLElement)?.className === 'fonticonbox') {
+ setTimeout(() => this._divRef?.focus());
+ break;
+ }
+ }
+ }
+ };
+
render() {
TraceMobx();
const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
- const label = this.Title.startsWith('#') ? null : this.Title;
return (
- <div key={label?.length} className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string }}>
+ <div className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this.boxShadow }}>
<div
className="labelBox-mainButton"
style={{
backgroundColor: this.backgroundColor,
- // fontSize: StrCast(this.layoutDoc._text_fontSize),
- color: StrCast(this.layoutDoc._color),
- fontFamily: StrCast(this.layoutDoc._text_fontFamily) || 'inherit',
+ color: StrCast(this.layoutDoc._text_fontColor, StrCast(this.layoutDoc._color)),
+ fontFamily: StrCast(this.layoutDoc._text_fontFamily, StrCast(Doc.UserDoc().fontFamily)) || 'inherit',
letterSpacing: StrCast(this.layoutDoc.letterSpacing),
- textTransform: StrCast(this.layoutDoc.textTransform) as Property.TextTransform,
+ 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),
width: this._props.PanelWidth(),
height: this._props.PanelHeight(),
- whiteSpace: 'multiLine' in boxParams && boxParams.multiLine ? 'pre-wrap' : 'pre',
+ whiteSpace: boxParams.multiLine ? 'pre-wrap' : 'pre',
}}>
<div
+ key={this._forceRerender}
style={{
width: this._props.PanelWidth() - 2 * NumCast(this.layoutDoc._xPadding),
height: this._props.PanelHeight() - 2 * NumCast(this.layoutDoc._yPadding),
outline: 'unset !important',
}}
- onKeyDown={action(e => {
+ onKeyDown={e => {
e.stopPropagation();
- })}
+ }}
onKeyUp={action(e => {
e.stopPropagation();
- this.dataDoc[this.fieldKey] = this._divRef?.innerText ?? '';
- setTimeout(() => this._props.select(false));
+ const text = this._divRef?.firstChild;
+ if (text && (text as HTMLElement)?.nodeType === 3) {
+ this._divRef?.removeChild(text);
+ this._divRef?.firstChild?.appendChild(text);
+ this.resetCursor();
+ }
+ this.fitTextToBox(this._divRef);
})}
+ onFocus={() => {
+ RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, this.dataDoc);
+ this._divRef?.removeEventListener('focusout', this.keepFocus);
+ this._divRef?.addEventListener('focusout', this.keepFocus);
+ }}
onBlur={() => {
- this.dataDoc[this.fieldKey] = this._divRef?.innerText ?? '';
+ this._divRef?.removeEventListener('focusout', this.keepFocus);
+ this.setText(this._divRef?.innerText ?? '');
+ 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('#') ? null : (this.Title ?? '')}</span>`,
}}
contentEditable={this._props.onClickScript?.() ? undefined : true}
ref={r => {
+ this._divRef?.removeEventListener('beforeinput', this.beforeInput);
this._divRef = r;
- this.fitTextToBox(r);
- if (this._props.isSelected() && this._divRef) {
- const range = document.createRange();
- range.setStart(this._divRef, this._divRef.childNodes.length);
- range.setEnd(this._divRef, this._divRef.childNodes.length);
- const sel = window.getSelection();
- sel?.removeAllRanges();
- sel?.addRange(range);
+ if (this._divRef) {
+ this._divRef.addEventListener('beforeinput', this.beforeInput);
+
+ if (Doc.SelectOnLoad === this.Document) {
+ Doc.SelectOnLoad = undefined;
+ this._divRef.focus();
+ }
+ this.fitTextToBox(this._divRef);
+ if (this.Title) {
+ this.resetCursor();
+ }
}
- }}>
- {label}
- </div>
+ }}
+ />
</div>
</div>
);
@@ -161,9 +249,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
Docs.Prototypes.TemplateMap.set(DocumentType.LABEL, {
layout: { view: LabelBox, dataField: 'title' },
- options: { acl: '', _singleLine: true, _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true },
+ options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' },
});
Docs.Prototypes.TemplateMap.set(DocumentType.BUTTON, {
layout: { view: LabelBox, dataField: 'title' },
- options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true },
+ options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' },
});
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index c66f7c726..4a436319b 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -428,7 +428,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
};
- getView = async (doc: Doc, options: FocusViewOptions) => {
+ getView = (doc: Doc, options: FocusViewOptions) => {
if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
this.toggleSidebar();
options.didMove = true;
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index a4557196e..d5d8f8afa 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -383,7 +383,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
}
};
- getView = async (doc: Doc, options: FocusViewOptions) => {
+ getView = (doc: Doc, options: FocusViewOptions) => {
if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
this.toggleSidebar();
options.didMove = true;
@@ -732,7 +732,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
MapBoxContainer._rerenderDelay = 0;
}
this._rerenderTimeout = undefined;
- // eslint-disable-next-line operator-assignment
this.Document[DocCss] = this.Document[DocCss] + 1;
}), MapBoxContainer._rerenderDelay);
return null;
@@ -792,7 +791,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
.map(pushpin => (
<DocumentView
key={pushpin[Id]}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
renderDepth={this._props.renderDepth + 1}
Document={pushpin}
@@ -830,7 +828,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
<div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
<SidebarAnnos
ref={this._sidebarRef}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
fieldKey={this.fieldKey}
Document={this.Document}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 816d4a3b0..2e4c87d38 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -233,7 +233,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options);
};
- getView = async (doc: Doc, options: FocusViewOptions) => {
+ getView = (doc: Doc, options: FocusViewOptions) => {
if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
options.didMove = true;
this.toggleSidebar(false);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index de51f6447..3bf7de2fe 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -776,7 +776,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// starts marquee selection
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
setupMoveUpEvents(
this,
e,
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index a5788d02a..9ba4f8ead 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -44,6 +44,7 @@ import { LinkInfo } from './LinkDocPreview';
import { OpenWhere } from './OpenWhere';
import './WebBox.scss';
+// eslint-disable-next-line @typescript-eslint/no-require-imports
const { CreateImage } = require('./WebBoxRenderer');
@observer
@@ -335,7 +336,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
ele = document.createElement('div');
ele.append(contents);
}
- } catch (e) {
+ } catch {
/* empty */
}
const visibleAnchor = this._getAnchor(this._savedAnnotations, true);
@@ -506,7 +507,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
let href: Opt<string>;
try {
href = iframe?.contentWindow?.location.href;
- } catch (e) {
+ } catch {
runInAction(() => this._warning++);
href = undefined;
}
@@ -713,7 +714,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._webUrl = this._url;
}
}
- } catch (e) {
+ } catch {
console.log('WebBox URL error:' + this._url);
}
return true;
@@ -805,7 +806,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
sel.empty(); // Chrome
else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
this.marqueeing = [e.clientX, e.clientY];
- if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
setupMoveUpEvents(
this,
e,
@@ -855,7 +856,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
})}
contentEditable
onPointerDown={this.webClipDown}
- // eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: field.html }}
/>
);
@@ -1031,7 +1031,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
{this.inlineTextAnnotations
.sort((a, b) => NumCast(a.y) - NumCast(b.y))
.map(anno => (
- // eslint-disable-next-line react/jsx-props-no-spreading
<Annotation {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} containerDataDoc={this.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} />
))}
</div>
@@ -1042,7 +1041,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
renderAnnotations = (childFilters: () => string[]) => (
<CollectionFreeFormView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
setContentViewBox={this.setInnerContent}
NativeWidth={returnZero}
@@ -1217,7 +1215,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>
<SidebarAnnos
ref={this._sidebarRef}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
fieldKey={this.fieldKey + '_' + this._urlHash}
diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
index ccf9caf15..34e7cf5ea 100644
--- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts
+++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
@@ -11,9 +11,11 @@ import { NoTool } from '../tools/NoTool';
import { RAGTool } from '../tools/RAGTool';
import { SearchTool } from '../tools/SearchTool';
import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool';
-import { AgentMessage, AssistantMessage, PROCESSING_TYPE, ProcessingInfo, Tool } from '../types/types';
+import { AgentMessage, AssistantMessage, Observation, PROCESSING_TYPE, ProcessingInfo } from '../types/types';
import { Vectorstore } from '../vectorstore/Vectorstore';
import { getReactPrompt } from './prompts';
+import { BaseTool } from '../tools/BaseTool';
+import { Parameter, ParametersType, Tool } from '../tools/ToolTypes';
dotenv.config();
@@ -24,7 +26,6 @@ dotenv.config();
export class Agent {
// Private properties
private client: OpenAI;
- private tools: Record<string, Tool<any>>; // bcz: need a real type here
private messages: AgentMessage[] = [];
private interMessages: AgentMessage[] = [];
private vectorstore: Vectorstore;
@@ -36,6 +37,7 @@ export class Agent {
private processingNumber: number = 0;
private processingInfo: ProcessingInfo[] = [];
private streamedAnswerParser: StreamedAnswerParser = new StreamedAnswerParser();
+ private tools: Record<string, BaseTool<ReadonlyArray<Parameter>>>;
/**
* The constructor initializes the agent with the vector store and toolset, and sets up the OpenAI client.
@@ -108,15 +110,16 @@ export class Agent {
let currentAction: string | undefined;
this.processingInfo = [];
- // Conversation loop (up to maxTurns)
- for (let i = 2; i < maxTurns; i += 2) {
+ let i = 2;
+ while (i < maxTurns) {
console.log(this.interMessages);
console.log(`Turn ${i}/${maxTurns}`);
- // Execute a step in the conversation and get the result
const result = await this.execute(onProcessingUpdate, onAnswerUpdate);
this.interMessages.push({ role: 'assistant', content: result });
+ i += 2;
+
let parsedResult;
try {
// Parse XML result from the assistant
@@ -148,7 +151,7 @@ export class Agent {
{
type: 'text',
text: `<stage number="${i + 1}" role="user">` + builder.build({ action_rules: this.tools[currentAction].getActionRule() }) + `</stage>`,
- },
+ } as Observation,
];
this.interMessages.push({ role: 'user', content: nextPrompt });
break;
@@ -166,8 +169,8 @@ export class Agent {
if (currentAction) {
try {
// Process the action with its input
- const observation = (await this.processAction(currentAction, actionInput.inputs)) as any; // bcz: really need a type here
- const nextPrompt = [{ type: 'text', text: `<stage number="${i + 1}" role="user"> <observation>` }, ...observation, { type: 'text', text: '</observation></stage>' }];
+ const observation = (await this.processAction(currentAction, actionInput.inputs)) as Observation[];
+ const nextPrompt = [{ type: 'text', text: `<stage number="${i + 1}" role="user"> <observation>` }, ...observation, { type: 'text', text: '</observation></stage>' }] as Observation[];
console.log(observation);
this.interMessages.push({ role: 'user', content: nextPrompt });
this.processingNumber++;
@@ -262,16 +265,69 @@ export class Agent {
/**
* Processes a specific action by invoking the appropriate tool with the provided inputs.
- * @param action The action to perform.
- * @param actionInput The inputs for the action.
- * @returns The result of the action.
+ * This method ensures that the action exists and validates the types of `actionInput`
+ * based on the tool's parameter rules. It throws errors for missing required parameters
+ * or mismatched types before safely executing the tool with the validated input.
+ *
+ * Type validation includes checks for:
+ * - `string`, `number`, `boolean`
+ * - `string[]`, `number[]` (arrays of strings or numbers)
+ *
+ * @param action The action to perform. It corresponds to a registered tool.
+ * @param actionInput The inputs for the action, passed as an object where each key is a parameter name.
+ * @returns A promise that resolves to an array of `Observation` objects representing the result of the action.
+ * @throws An error if the action is unknown, if required parameters are missing, or if input types don't match the expected parameter types.
*/
- private async processAction(action: string, actionInput: unknown): Promise<unknown> {
+ private async processAction(action: string, actionInput: Record<string, unknown>): Promise<Observation[]> {
+ // Check if the action exists in the tools list
if (!(action in this.tools)) {
throw new Error(`Unknown action: ${action}`);
}
const tool = this.tools[action];
- return await tool.execute(actionInput);
+
+ // Validate actionInput based on tool's parameter rules
+ for (const paramRule of tool.parameterRules) {
+ const inputValue = actionInput[paramRule.name];
+
+ if (paramRule.required && inputValue === undefined) {
+ throw new Error(`Missing required parameter: ${paramRule.name}`);
+ }
+
+ // If the parameter is defined, check its type
+ if (inputValue !== undefined) {
+ switch (paramRule.type) {
+ case 'string':
+ if (typeof inputValue !== 'string') {
+ throw new Error(`Expected parameter '${paramRule.name}' to be a string.`);
+ }
+ break;
+ case 'number':
+ if (typeof inputValue !== 'number') {
+ throw new Error(`Expected parameter '${paramRule.name}' to be a number.`);
+ }
+ break;
+ case 'boolean':
+ if (typeof inputValue !== 'boolean') {
+ throw new Error(`Expected parameter '${paramRule.name}' to be a boolean.`);
+ }
+ break;
+ case 'string[]':
+ if (!Array.isArray(inputValue) || !inputValue.every(item => typeof item === 'string')) {
+ throw new Error(`Expected parameter '${paramRule.name}' to be an array of strings.`);
+ }
+ break;
+ case 'number[]':
+ if (!Array.isArray(inputValue) || !inputValue.every(item => typeof item === 'number')) {
+ throw new Error(`Expected parameter '${paramRule.name}' to be an array of numbers.`);
+ }
+ break;
+ default:
+ throw new Error(`Unsupported parameter type: ${paramRule.type}`);
+ }
+ }
+ }
+
+ return await tool.execute(actionInput as ParametersType<typeof tool.parameterRules>);
}
}
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx
index d48f46963..e463d15bf 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx
@@ -23,7 +23,6 @@ import ReactMarkdown from 'react-markdown';
*/
interface MessageComponentProps {
message: AssistantMessage;
- index: number;
onFollowUpClick: (question: string) => void;
onCitationClick: (citation: Citation) => void;
updateMessageCitations: (index: number, citations: Citation[]) => void;
@@ -34,7 +33,7 @@ interface MessageComponentProps {
* processing information, and follow-up questions.
* @param {MessageComponentProps} props - The props for the component.
*/
-const MessageComponentBox: React.FC<MessageComponentProps> = ({ message, index, onFollowUpClick, onCitationClick, updateMessageCitations }) => {
+const MessageComponentBox: React.FC<MessageComponentProps> = ({ message, onFollowUpClick, onCitationClick }) => {
// State for managing whether the dropdown is open or closed for processing info
const [dropdownOpen, setDropdownOpen] = useState(false);
diff --git a/src/client/views/nodes/chatbot/tools/BaseTool.ts b/src/client/views/nodes/chatbot/tools/BaseTool.ts
index a77f567a5..58cd514d9 100644
--- a/src/client/views/nodes/chatbot/tools/BaseTool.ts
+++ b/src/client/views/nodes/chatbot/tools/BaseTool.ts
@@ -1,32 +1,78 @@
+import { Observation } from '../types/types';
+import { Parameter, Tool, ParametersType } from './ToolTypes';
+
/**
* @file BaseTool.ts
- * @description This file defines the abstract BaseTool class, which serves as a blueprint
+ * @description This file defines the abstract `BaseTool` class, which serves as a blueprint
* for tool implementations in the AI assistant system. Each tool has a name, description,
- * parameters, and citation rules. The BaseTool class provides a structure for executing actions
+ * parameters, and citation rules. The `BaseTool` class provides a structure for executing actions
* and retrieving action rules for use within the assistant's workflow.
*/
-import { Tool } from '../types/types';
+/**
+ * The `BaseTool` class is an abstract class that implements the `Tool` interface.
+ * It is generic over a type parameter `P`, which extends `ReadonlyArray<Parameter>`.
+ * This means `P` is a readonly array of `Parameter` objects that cannot be modified (immutable).
+ */
+export abstract class BaseTool<P extends ReadonlyArray<Parameter>> implements Tool<P> {
+ // The name of the tool (e.g., "calculate", "searchTool")
+ name: string;
+ // A description of the tool's functionality
+ description: string;
+ // An array of parameter definitions for the tool
+ parameterRules: P;
+ // Guidelines for how to handle citations when using the tool
+ citationRules: string;
+ // A brief summary of the tool's purpose
+ briefSummary: string;
-export abstract class BaseTool<T extends Record<string, unknown> = Record<string, unknown>> implements Tool<T> {
- constructor(
- public name: string,
- public description: string,
- public parameters: Record<string, unknown>,
- public citationRules: string,
- public briefSummary: string
- ) {}
+ /**
+ * Constructs a new `BaseTool` instance.
+ * @param name - The name of the tool.
+ * @param description - A detailed description of what the tool does.
+ * @param parameterRules - A readonly array of parameter definitions (`ReadonlyArray<Parameter>`).
+ * @param citationRules - Rules or guidelines for citations.
+ * @param briefSummary - A short summary of the tool.
+ */
+ constructor(name: string, description: string, parameterRules: P, citationRules: string, briefSummary: string) {
+ this.name = name;
+ this.description = description;
+ this.parameterRules = parameterRules;
+ this.citationRules = citationRules;
+ this.briefSummary = briefSummary;
+ }
- abstract execute(args: T): Promise<unknown>;
+ /**
+ * The `execute` method is abstract and must be implemented by subclasses.
+ * It defines the action the tool performs when executed.
+ * @param args - The arguments for the tool's execution, whose types are inferred from `ParametersType<P>`.
+ * @returns A promise that resolves to an array of `Observation` objects.
+ */
+ abstract execute(args: ParametersType<P>): Promise<Observation[]>;
+ /**
+ * Generates an action rule object that describes the tool's usage.
+ * This is useful for dynamically generating documentation or for tools that need to expose their parameters at runtime.
+ * @returns An object containing the tool's name, description, and parameter definitions.
+ */
getActionRule(): Record<string, unknown> {
return {
- [this.name]: {
- name: this.name,
- citationRules: this.citationRules,
- description: this.description,
- parameters: this.parameters,
- },
+ tool: this.name,
+ description: this.description,
+ parameters: this.parameterRules.reduce(
+ (acc, param) => {
+ // Build an object for each parameter without the 'name' property, since it's used as the key
+ acc[param.name] = {
+ type: param.type,
+ description: param.description,
+ required: param.required,
+ // Conditionally include 'max_inputs' only if it is defined
+ ...(param.max_inputs !== undefined && { max_inputs: param.max_inputs }),
+ } as Omit<P[number], 'name'>; // Type assertion to exclude the 'name' property
+ return acc;
+ },
+ {} as Record<string, Omit<P[number], 'name'>> // Initialize the accumulator as an empty object
+ ),
};
}
}
diff --git a/src/client/views/nodes/chatbot/tools/CalculateTool.ts b/src/client/views/nodes/chatbot/tools/CalculateTool.ts
index 77ab1b39b..e96c9a98a 100644
--- a/src/client/views/nodes/chatbot/tools/CalculateTool.ts
+++ b/src/client/views/nodes/chatbot/tools/CalculateTool.ts
@@ -1,26 +1,32 @@
+import { Observation } from '../types/types';
+import { ParametersType } from './ToolTypes';
import { BaseTool } from './BaseTool';
-export class CalculateTool extends BaseTool<{ expression: string }> {
+const calculateToolParams = [
+ {
+ name: 'expression',
+ type: 'string',
+ description: 'The mathematical expression to evaluate',
+ required: true,
+ },
+] as const;
+
+type CalculateToolParamsType = typeof calculateToolParams;
+
+export class CalculateTool extends BaseTool<CalculateToolParamsType> {
constructor() {
super(
'calculate',
'Perform a calculation',
- {
- expression: {
- type: 'string',
- description: 'The mathematical expression to evaluate',
- required: 'true',
- max_inputs: '1',
- },
- },
+ calculateToolParams, // Use the reusable param config here
'Provide a mathematical expression to calculate that would work with JavaScript eval().',
'Runs a calculation and returns the number - uses JavaScript so be sure to use floating point syntax if necessary'
);
}
- async execute(args: { expression: string }): Promise<unknown> {
- // Note: Using eval() can be dangerous. Consider using a safer alternative.
- const result = eval(args.expression);
+ async execute(args: ParametersType<CalculateToolParamsType>): Promise<Observation[]> {
+ // TypeScript will ensure 'args.expression' is a string based on the param config
+ const result = eval(args.expression); // Be cautious with eval(), as it can be dangerous. Consider using a safer alternative.
return [{ type: 'text', text: result.toString() }];
}
}
diff --git a/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts b/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts
index d3ded0de0..b321d98ba 100644
--- a/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts
+++ b/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts
@@ -1,40 +1,47 @@
import { BaseTool } from './BaseTool';
import { Networking } from '../../../../Network';
+import { Observation } from '../types/types';
+import { ParametersType } from './ToolTypes';
-export class CreateCSVTool extends BaseTool<{ csvData: string; filename: string }> {
+const createCSVToolParams = [
+ {
+ name: 'csvData',
+ type: 'string',
+ description: 'A string of comma-separated values representing the CSV data.',
+ required: true,
+ },
+ {
+ name: 'filename',
+ type: 'string',
+ description: 'The base name of the CSV file to be created. Should end in ".csv".',
+ required: true,
+ },
+] as const;
+
+type CreateCSVToolParamsType = typeof createCSVToolParams;
+
+export class CreateCSVTool extends BaseTool<CreateCSVToolParamsType> {
private _handleCSVResult: (url: string, filename: string, id: string, data: string) => void;
constructor(handleCSVResult: (url: string, title: string, id: string, data: string) => void) {
super(
'createCSV',
'Creates a CSV file from raw CSV data and saves it to the server',
- {
- type: 'object',
- properties: {
- csvData: {
- type: 'string',
- description: 'A string of comma-separated values representing the CSV data.',
- },
- filename: {
- type: 'string',
- description: 'The base name of the CSV file to be created. Should end in ".csv".',
- },
- },
- required: ['csvData', 'filename'],
- },
+ createCSVToolParams,
'Provide a CSV string and a filename to create a CSV file.',
'Creates a CSV file from the provided CSV string and saves it to the server with a unique identifier, returning the file URL and UUID.'
);
this._handleCSVResult = handleCSVResult;
}
- async execute(args: { csvData: string; filename: string }): Promise<unknown> {
+ async execute(args: ParametersType<CreateCSVToolParamsType>): Promise<Observation[]> {
try {
console.log('Creating CSV file:', args.filename, ' with data:', args.csvData);
- // Post the raw CSV data to the createCSV endpoint on the server
- const { fileUrl, id } = await Networking.PostToServer('/createCSV', { filename: args.filename, data: args.csvData });
+ const { fileUrl, id } = await Networking.PostToServer('/createCSV', {
+ filename: args.filename,
+ data: args.csvData,
+ });
- // Handle the result by invoking the callback
this._handleCSVResult(fileUrl, args.filename, id, args.csvData);
return [
diff --git a/src/client/views/nodes/chatbot/tools/CreateCollectionTool.ts b/src/client/views/nodes/chatbot/tools/CreateCollectionTool.ts
deleted file mode 100644
index 1e479a62c..000000000
--- a/src/client/views/nodes/chatbot/tools/CreateCollectionTool.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { DocCast } from '../../../../../fields/Types';
-import { DocServer } from '../../../../DocServer';
-import { Docs } from '../../../../documents/Documents';
-import { DocumentView } from '../../DocumentView';
-import { OpenWhere } from '../../OpenWhere';
-import { BaseTool } from './BaseTool';
-
-export class GetDocsContentTool extends BaseTool<{ title: string; document_ids: string[] }> {
- private _docView: DocumentView;
- constructor(docView: DocumentView) {
- super(
- 'retrieveDocs',
- 'Retrieves the contents of all Documents that the user is interacting with in Dash ',
- {
- title: {
- type: 'string',
- description: 'the title of the collection that you will be making',
- required: 'true',
- max_inputs: '1',
- },
- },
- 'Provide a mathematical expression to calculate that would work with JavaScript eval().',
- 'Runs a calculation and returns the number - uses JavaScript so be sure to use floating point syntax if necessary'
- );
- this._docView = docView;
- }
-
- async execute(args: { title: string; document_ids: string[] }): Promise<unknown> {
- // Note: Using eval() can be dangerous. Consider using a safer alternative.
- const docs = args.document_ids.map(doc_id => DocCast(DocServer.GetCachedRefField(doc_id)));
- const collection = Docs.Create.FreeformDocument(docs, { title: args.title });
- this._docView._props.addDocTab(collection, OpenWhere.addRight); //in future, create popup prompting user where to add
- return [{ type: 'text', text: 'Collection created in Dash called ' + args.title }];
- }
-}
-//export function create_collection(docView: DocumentView, document_ids: string[], title: string): string {}
diff --git a/src/client/views/nodes/chatbot/tools/DataAnalysisTool.ts b/src/client/views/nodes/chatbot/tools/DataAnalysisTool.ts
index 2e663fed1..d9b75219d 100644
--- a/src/client/views/nodes/chatbot/tools/DataAnalysisTool.ts
+++ b/src/client/views/nodes/chatbot/tools/DataAnalysisTool.ts
@@ -1,22 +1,29 @@
+import { Observation } from '../types/types';
+import { ParametersType } from './ToolTypes';
import { BaseTool } from './BaseTool';
-export class DataAnalysisTool extends BaseTool<{ csv_file_name: string | string[] }> {
+const dataAnalysisToolParams = [
+ {
+ name: 'csv_file_names',
+ type: 'string[]',
+ description: 'List of names of the CSV files to analyze',
+ required: true,
+ max_inputs: 3,
+ },
+] as const;
+
+type DataAnalysisToolParamsType = typeof dataAnalysisToolParams;
+
+export class DataAnalysisTool extends BaseTool<DataAnalysisToolParamsType> {
private csv_files_function: () => { filename: string; id: string; text: string }[];
constructor(csv_files: () => { filename: string; id: string; text: string }[]) {
super(
'dataAnalysis',
- 'Analyzes, and provides insights, from one or more CSV files',
- {
- csv_file_name: {
- type: 'string',
- description: 'Name(s) of the CSV file(s) to analyze',
- required: 'true',
- max_inputs: '3',
- },
- },
+ 'Analyzes and provides insights from one or more CSV files',
+ dataAnalysisToolParams,
'Provide the name(s) of up to 3 CSV files to analyze based on the user query and whichever available CSV files may be relevant.',
- 'Provides the full CSV file text for your analysis based on the user query and the available CSV file(s). '
+ 'Provides the full CSV file text for your analysis based on the user query and the available CSV file(s).'
);
this.csv_files_function = csv_files;
}
@@ -33,9 +40,9 @@ export class DataAnalysisTool extends BaseTool<{ csv_file_name: string | string[
return file?.id;
}
- async execute(args: { csv_file_name: string | string[] }): Promise<unknown> {
- const filenames = Array.isArray(args.csv_file_name) ? args.csv_file_name : [args.csv_file_name];
- const results = [];
+ async execute(args: ParametersType<DataAnalysisToolParamsType>): Promise<Observation[]> {
+ const filenames = args.csv_file_names;
+ const results: Observation[] = [];
for (const filename of filenames) {
const fileContent = this.getFileContent(filename);
@@ -44,7 +51,7 @@ export class DataAnalysisTool extends BaseTool<{ csv_file_name: string | string[
if (fileContent && fileID) {
results.push({
type: 'text',
- text: `<chunk chunk_id=${fileID} chunk_type=csv>${fileContent}</chunk>`,
+ text: `<chunk chunk_id="${fileID}" chunk_type="csv">${fileContent}</chunk>`,
});
} else {
results.push({
diff --git a/src/client/views/nodes/chatbot/tools/GetDocsTool.ts b/src/client/views/nodes/chatbot/tools/GetDocsTool.ts
index 903f3f69c..26756522c 100644
--- a/src/client/views/nodes/chatbot/tools/GetDocsTool.ts
+++ b/src/client/views/nodes/chatbot/tools/GetDocsTool.ts
@@ -1,29 +1,47 @@
-import { DocCast } from '../../../../../fields/Types';
+import { Observation } from '../types/types';
+import { ParametersType } from './ToolTypes';
+import { BaseTool } from './BaseTool';
import { DocServer } from '../../../../DocServer';
import { Docs } from '../../../../documents/Documents';
import { DocumentView } from '../../DocumentView';
import { OpenWhere } from '../../OpenWhere';
-import { BaseTool } from './BaseTool';
+import { DocCast } from '../../../../../fields/Types';
-export class GetDocsTool extends BaseTool<{ title: string; document_ids: string[] }> {
+const getDocsToolParams = [
+ {
+ name: 'title',
+ type: 'string',
+ description: 'Title of the collection being created from retrieved documents',
+ required: true,
+ },
+ {
+ name: 'document_ids',
+ type: 'string[]',
+ description: 'List of document IDs to retrieve',
+ required: true,
+ },
+] as const;
+
+type GetDocsToolParamsType = typeof getDocsToolParams;
+
+export class GetDocsTool extends BaseTool<GetDocsToolParamsType> {
private _docView: DocumentView;
+
constructor(docView: DocumentView) {
super(
'retrieveDocs',
'Retrieves the contents of all Documents that the user is interacting with in Dash',
- {},
+ getDocsToolParams,
'No need to provide anything. Just run the tool and it will retrieve the contents of all Documents that the user is interacting with in Dash.',
- 'Returns the the documents in Dash in JSON form. This will include the title of the document, the location in the FreeFormDocument, and the content of the document, any applicable data fields, the layout of the document, etc.'
+ 'Returns the documents in Dash in JSON form.'
);
this._docView = docView;
}
- async execute(args: { title: string; document_ids: string[] }): Promise<unknown> {
- // Note: Using eval() can be dangerous. Consider using a safer alternative.
+ async execute(args: ParametersType<GetDocsToolParamsType>): Promise<Observation[]> {
const docs = args.document_ids.map(doc_id => DocCast(DocServer.GetCachedRefField(doc_id)));
const collection = Docs.Create.FreeformDocument(docs, { title: args.title });
- this._docView._props.addDocTab(collection, OpenWhere.addRight); //in future, create popup prompting user where to add
- return [{ type: 'text', text: 'Collection created in Dash called ' + args.title }];
+ this._docView._props.addDocTab(collection, OpenWhere.addRight);
+ return [{ type: 'text', text: `Collection created in Dash called ${args.title}` }];
}
}
-//export function create_collection(docView: DocumentView, document_ids: string[], title: string): string {}
diff --git a/src/client/views/nodes/chatbot/tools/NoTool.ts b/src/client/views/nodes/chatbot/tools/NoTool.ts
index edd3160ec..a92e3fa23 100644
--- a/src/client/views/nodes/chatbot/tools/NoTool.ts
+++ b/src/client/views/nodes/chatbot/tools/NoTool.ts
@@ -1,19 +1,18 @@
-// tools/NoTool.ts
import { BaseTool } from './BaseTool';
+import { Observation } from '../types/types';
+import { ParametersType } from './ToolTypes';
-export class NoTool extends BaseTool<Record<string, unknown>> {
+const noToolParams = [] as const;
+
+type NoToolParamsType = typeof noToolParams;
+
+export class NoTool extends BaseTool<NoToolParamsType> {
constructor() {
- super(
- 'no_tool',
- 'Use this when no external tool or action is required to answer the question.',
- {},
- 'When using the "no_tool" action, simply provide an empty <action_input> element. The observation will always be "No tool used. Proceed with answering the question."',
- 'Use when no external tool or action is required to answer the question.'
- );
+ super('noTool', 'A placeholder tool that performs no action', noToolParams, 'This tool does not require any input or perform any action.', 'Does nothing.');
}
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- async execute(args: object): Promise<unknown> {
- return [{ type: 'text', text: 'No tool used. Proceed with answering the question.' }];
+ async execute(args: ParametersType<NoToolParamsType>): Promise<Observation[]> {
+ // Since there are no parameters, args will be an empty object
+ return [{ type: 'text', text: 'This tool does nothing.' }];
}
}
diff --git a/src/client/views/nodes/chatbot/tools/RAGTool.ts b/src/client/views/nodes/chatbot/tools/RAGTool.ts
index 4cc2f26ff..482069f36 100644
--- a/src/client/views/nodes/chatbot/tools/RAGTool.ts
+++ b/src/client/views/nodes/chatbot/tools/RAGTool.ts
@@ -1,20 +1,26 @@
import { Networking } from '../../../../Network';
-import { RAGChunk } from '../types/types';
+import { Observation, RAGChunk } from '../types/types';
+import { ParametersType } from './ToolTypes';
import { Vectorstore } from '../vectorstore/Vectorstore';
import { BaseTool } from './BaseTool';
-export class RAGTool extends BaseTool {
+const ragToolParams = [
+ {
+ name: 'hypothetical_document_chunk',
+ type: 'string',
+ description: "A detailed prompt representing an ideal chunk to embed and compare against document vectors to retrieve the most relevant content for answering the user's query.",
+ required: true,
+ },
+] as const;
+
+type RAGToolParamsType = typeof ragToolParams;
+
+export class RAGTool extends BaseTool<RAGToolParamsType> {
constructor(private vectorstore: Vectorstore) {
super(
'rag',
'Perform a RAG search on user documents',
- {
- hypothetical_document_chunk: {
- type: 'string',
- description: "A detailed prompt representing an ideal chunk to embed and compare against document vectors to retrieve the most relevant content for answering the user's query.",
- required: 'true',
- },
- },
+ ragToolParams,
`
When using the RAG tool, the structure must adhere to the format described in the ReAct prompt. Below are additional guidelines specifically for RAG-based responses:
@@ -51,18 +57,17 @@ export class RAGTool extends BaseTool {
</follow_up_questions>
</answer>
`,
-
`Performs a RAG (Retrieval-Augmented Generation) search on user documents and returns a set of document chunks (text or images) to provide a grounded response based on user documents.`
);
}
- async execute(args: { hypothetical_document_chunk: string }): Promise<unknown> {
+ async execute(args: ParametersType<RAGToolParamsType>): Promise<Observation[]> {
const relevantChunks = await this.vectorstore.retrieve(args.hypothetical_document_chunk);
- const formatted_chunks = await this.getFormattedChunks(relevantChunks);
- return formatted_chunks;
+ const formattedChunks = await this.getFormattedChunks(relevantChunks);
+ return formattedChunks;
}
- async getFormattedChunks(relevantChunks: RAGChunk[]): Promise<unknown> {
+ async getFormattedChunks(relevantChunks: RAGChunk[]): Promise<Observation[]> {
try {
const { formattedChunks } = await Networking.PostToServer('/formatChunks', { relevantChunks });
diff --git a/src/client/views/nodes/chatbot/tools/SearchTool.ts b/src/client/views/nodes/chatbot/tools/SearchTool.ts
index 3a4668422..fd5144dd6 100644
--- a/src/client/views/nodes/chatbot/tools/SearchTool.ts
+++ b/src/client/views/nodes/chatbot/tools/SearchTool.ts
@@ -1,53 +1,68 @@
import { v4 as uuidv4 } from 'uuid';
import { Networking } from '../../../../Network';
import { BaseTool } from './BaseTool';
+import { Observation } from '../types/types';
+import { ParametersType } from './ToolTypes';
-export class SearchTool extends BaseTool<{ query: string | string[] }> {
+const searchToolParams = [
+ {
+ name: 'query',
+ type: 'string[]',
+ description: 'The search query or queries to use for finding websites',
+ required: true,
+ max_inputs: 3,
+ },
+] as const;
+
+type SearchToolParamsType = typeof searchToolParams;
+
+export class SearchTool extends BaseTool<SearchToolParamsType> {
private _addLinkedUrlDoc: (url: string, id: string) => void;
private _max_results: number;
+
constructor(addLinkedUrlDoc: (url: string, id: string) => void, max_results: number = 5) {
super(
'searchTool',
'Search the web to find a wide range of websites related to a query or multiple queries',
- {
- query: {
- type: 'string',
- description: 'The search query or queries to use for finding websites',
- required: 'true',
- max_inputs: '3',
- },
- },
- 'Provide up to 3 search queries to find a broad range of websites. This tool is intended to help you identify relevant websites, but not to be used for providing the final answer. Use this information to determine which specific website to investigate further.',
- 'Returns a list of websites and their overviews based on the search queries, helping to identify which websites might contain relevant information.'
+ searchToolParams,
+ 'Provide up to 3 search queries to find a broad range of websites.',
+ 'Returns a list of websites and their overviews based on the search queries.'
);
this._addLinkedUrlDoc = addLinkedUrlDoc;
this._max_results = max_results;
}
- async execute(args: { query: string | string[] }): Promise<unknown> {
- const queries = Array.isArray(args.query) ? args.query : [args.query];
- const allResults = [];
+ async execute(args: ParametersType<SearchToolParamsType>): Promise<Observation[]> {
+ const queries = args.query;
- for (const query of queries) {
+ // Create an array of promises, each one handling a search for a query
+ const searchPromises = queries.map(async query => {
try {
- const { results } = await Networking.PostToServer('/getWebSearchResults', { query, max_results: this._max_results });
- const data: { type: string; text: string }[] = results.map((result: { url: string; snippet: string }) => {
+ const { results } = await Networking.PostToServer('/getWebSearchResults', {
+ query,
+ max_results: this._max_results,
+ });
+ const data = results.map((result: { url: string; snippet: string }) => {
const id = uuidv4();
return {
type: 'text',
- text: `<chunk chunk_id="${id}" chunk_type="text">
- <url>${result.url}</url>
- <overview>${result.snippet}</overview>
- </chunk>`,
+ text: `<chunk chunk_id="${id}" chunk_type="text"><url>${result.url}</url><overview>${result.snippet}</overview></chunk>`,
};
});
- allResults.push(...data);
+ return data;
} catch (error) {
console.log(error);
- allResults.push({ type: 'text', text: `An error occurred while performing the web search for query: ${query}` });
+ return [
+ {
+ type: 'text',
+ text: `An error occurred while performing the web search for query: ${query}`,
+ },
+ ];
}
- }
+ });
+
+ const allResultsArrays = await Promise.all(searchPromises);
- return allResults;
+ return allResultsArrays.flat();
}
}
diff --git a/src/client/views/nodes/chatbot/tools/ToolTypes.ts b/src/client/views/nodes/chatbot/tools/ToolTypes.ts
new file mode 100644
index 000000000..d47a38952
--- /dev/null
+++ b/src/client/views/nodes/chatbot/tools/ToolTypes.ts
@@ -0,0 +1,76 @@
+import { Observation } from '../types/types';
+
+/**
+ * The `Tool` interface represents a generic tool in the system.
+ * It is generic over a type parameter `P`, which extends `ReadonlyArray<Parameter>`.
+ * @template P - An array of `Parameter` objects defining the tool's parameters.
+ */
+export interface Tool<P extends ReadonlyArray<Parameter>> {
+ // The name of the tool (e.g., "calculate", "searchTool")
+ name: string;
+ // A description of the tool's functionality
+ description: string;
+ // An array of parameter definitions for the tool
+ parameterRules: P;
+ // Guidelines for how to handle citations when using the tool
+ citationRules: string;
+ // A brief summary of the tool's purpose
+ briefSummary: string;
+ /**
+ * Executes the tool's main functionality.
+ * @param args - The arguments for execution, with types inferred from `ParametersType<P>`.
+ * @returns A promise that resolves to an array of `Observation` objects.
+ */
+ execute: (args: ParametersType<P>) => Promise<Observation[]>;
+ /**
+ * Generates an action rule object that describes the tool's usage.
+ * @returns An object representing the tool's action rules.
+ */
+ getActionRule: () => Record<string, unknown>;
+}
+
+/**
+ * The `Parameter` type defines the structure of a parameter configuration.
+ */
+export type Parameter = {
+ // The type of the parameter; constrained to the types 'string', 'number', 'boolean', 'string[]', 'number[]'
+ readonly type: 'string' | 'number' | 'boolean' | 'string[]' | 'number[]';
+ // The name of the parameter
+ readonly name: string;
+ // A description of the parameter
+ readonly description: string;
+ // Indicates whether the parameter is required
+ readonly required: boolean;
+ // (Optional) The maximum number of inputs (useful for array types)
+ readonly max_inputs?: number;
+};
+
+/**
+ * A utility type that maps string representations of types to actual TypeScript types.
+ * This is used to convert the `type` field of a `Parameter` into a concrete TypeScript type.
+ */
+type TypeMap = {
+ string: string;
+ number: number;
+ boolean: boolean;
+ 'string[]': string[];
+ 'number[]': number[];
+};
+
+/**
+ * The `ParamType` type maps a `Parameter`'s `type` field to the corresponding TypeScript type.
+ * If the `type` field matches a key in `TypeMap`, it returns the associated type.
+ * Otherwise, it returns `unknown`.
+ * @template P - A `Parameter` object.
+ */
+export type ParamType<P extends Parameter> = P['type'] extends keyof TypeMap ? TypeMap[P['type']] : unknown;
+
+/**
+ * The `ParametersType` type transforms an array of `Parameter` objects into an object type
+ * where each key is the parameter's name, and the value is the corresponding TypeScript type.
+ * This is used to define the types of the arguments passed to the `execute` method of a tool.
+ * @template P - An array of `Parameter` objects.
+ */
+export type ParametersType<P extends ReadonlyArray<Parameter>> = {
+ [K in P[number] as K['name']]: ParamType<K>;
+};
diff --git a/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts b/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts
index 1efb389b8..f2e3863a6 100644
--- a/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts
+++ b/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts
@@ -1,83 +1,99 @@
import { v4 as uuidv4 } from 'uuid';
import { Networking } from '../../../../Network';
import { BaseTool } from './BaseTool';
+import { Observation } from '../types/types';
+import { ParametersType } from './ToolTypes';
-export class WebsiteInfoScraperTool extends BaseTool<{ url: string | string[] }> {
+const websiteInfoScraperToolParams = [
+ {
+ name: 'urls',
+ type: 'string[]',
+ description: 'The URLs of the websites to scrape',
+ required: true,
+ max_inputs: 3,
+ },
+] as const;
+
+type WebsiteInfoScraperToolParamsType = typeof websiteInfoScraperToolParams;
+
+export class WebsiteInfoScraperTool extends BaseTool<WebsiteInfoScraperToolParamsType> {
private _addLinkedUrlDoc: (url: string, id: string) => void;
constructor(addLinkedUrlDoc: (url: string, id: string) => void) {
super(
'websiteInfoScraper',
'Scrape detailed information from specific websites relevant to the user query',
- {
- url: {
- type: 'string',
- description: 'The URL(s) of the website(s) to scrape',
- required: true,
- max_inputs: 3,
- },
- },
+ websiteInfoScraperToolParams,
`
- Your task is to provide a comprehensive response to the user's prompt using the content scraped from relevant websites. Ensure you follow these guidelines for structuring your response:
+ Your task is to provide a comprehensive response to the user's prompt using the content scraped from relevant websites. Ensure you follow these guidelines for structuring your response:
- 1. Grounded Text Tag Structure:
- - Wrap all text derived from the scraped website(s) in <grounded_text> tags.
- - **Do not include non-sourced information** in <grounded_text> tags.
- - Use a single <grounded_text> tag for content derived from a single website. If citing multiple websites, create new <grounded_text> tags for each.
- - Ensure each <grounded_text> tag has a citation index corresponding to the scraped URL.
+ 1. Grounded Text Tag Structure:
+ - Wrap all text derived from the scraped website(s) in <grounded_text> tags.
+ - **Do not include non-sourced information** in <grounded_text> tags.
+ - Use a single <grounded_text> tag for content derived from a single website. If citing multiple websites, create new <grounded_text> tags for each.
+ - Ensure each <grounded_text> tag has a citation index corresponding to the scraped URL.
- 2. Citation Tag Structure:
- - Create a <citation> tag for each distinct piece of information used from the website(s).
- - Each <citation> tag must reference a URL chunk using the chunk_id attribute.
- - For URL-based citations, leave the citation content empty, but reference the chunk_id and type as 'url'.
+ 2. Citation Tag Structure:
+ - Create a <citation> tag for each distinct piece of information used from the website(s).
+ - Each <citation> tag must reference a URL chunk using the chunk_id attribute.
+ - For URL-based citations, leave the citation content empty, but reference the chunk_id and type as 'url'.
- 3. Structural Integrity Checks:
- - Ensure all opening and closing tags are matched properly.
- - Verify that all citation_index attributes in <grounded_text> tags correspond to valid citations.
- - Do not over-cite—cite only the most relevant parts of the websites.
+ 3. Structural Integrity Checks:
+ - Ensure all opening and closing tags are matched properly.
+ - Verify that all citation_index attributes in <grounded_text> tags correspond to valid citations.
+ - Do not over-cite—cite only the most relevant parts of the websites.
- Example Usage:
+ Example Usage:
- <answer>
- <grounded_text citation_index="1">
- Based on data from the World Bank, economic growth has stabilized in recent years, following a surge in investments.
- </grounded_text>
- <grounded_text citation_index="2">
- According to information retrieved from the International Monetary Fund, the inflation rate has been gradually decreasing since 2020.
- </grounded_text>
+ <answer>
+ <grounded_text citation_index="1">
+ Based on data from the World Bank, economic growth has stabilized in recent years, following a surge in investments.
+ </grounded_text>
+ <grounded_text citation_index="2">
+ According to information retrieved from the International Monetary Fund, the inflation rate has been gradually decreasing since 2020.
+ </grounded_text>
- <citations>
- <citation index="1" chunk_id="1234" type="url"></citation>
- <citation index="2" chunk_id="5678" type="url"></citation>
- </citations>
+ <citations>
+ <citation index="1" chunk_id="1234" type="url"></citation>
+ <citation index="2" chunk_id="5678" type="url"></citation>
+ </citations>
- <follow_up_questions>
- <question>What are the long-term economic impacts of increased investments on GDP?</question>
- <question>How might inflation trends affect future monetary policy?</question>
- <question>Are there additional factors that could influence economic growth beyond investments and inflation?</question>
- </follow_up_questions>
- </answer>
- `,
+ <follow_up_questions>
+ <question>What are the long-term economic impacts of increased investments on GDP?</question>
+ <question>How might inflation trends affect future monetary policy?</question>
+ <question>Are there additional factors that could influence economic growth beyond investments and inflation?</question>
+ </follow_up_questions>
+ </answer>
+ `,
'Returns the text content of the webpages for further analysis and grounding.'
);
this._addLinkedUrlDoc = addLinkedUrlDoc;
}
- async execute(args: { url: string | string[] }): Promise<unknown> {
- const urls = Array.isArray(args.url) ? args.url : [args.url];
- const results = [];
+ async execute(args: ParametersType<WebsiteInfoScraperToolParamsType>): Promise<Observation[]> {
+ const urls = args.urls;
- for (const url of urls) {
+ // Create an array of promises, each one handling a website scrape for a URL
+ const scrapingPromises = urls.map(async url => {
try {
const { website_plain_text } = await Networking.PostToServer('/scrapeWebsite', { url });
const id = uuidv4();
this._addLinkedUrlDoc(url, id);
- results.push({ type: 'text', text: `<chunk chunk_id=${id} chunk_type=url>\n${website_plain_text}\n</chunk>\n` });
+ return {
+ type: 'text',
+ text: `<chunk chunk_id="${id}" chunk_type="url">\n${website_plain_text}\n</chunk>`,
+ } as Observation;
} catch (error) {
console.log(error);
- results.push({ type: 'text', text: `An error occurred while scraping the website: ${url}` });
+ return {
+ type: 'text',
+ text: `An error occurred while scraping the website: ${url}`,
+ } as Observation;
}
- }
+ });
+
+ // Wait for all scraping promises to resolve
+ const results = await Promise.all(scrapingPromises);
return results;
}
diff --git a/src/client/views/nodes/chatbot/tools/WikipediaTool.ts b/src/client/views/nodes/chatbot/tools/WikipediaTool.ts
index 692dff749..4fcffe2ed 100644
--- a/src/client/views/nodes/chatbot/tools/WikipediaTool.ts
+++ b/src/client/views/nodes/chatbot/tools/WikipediaTool.ts
@@ -1,33 +1,46 @@
import { v4 as uuidv4 } from 'uuid';
import { Networking } from '../../../../Network';
import { BaseTool } from './BaseTool';
+import { Observation } from '../types/types';
+import { ParametersType } from './ToolTypes';
-export class WikipediaTool extends BaseTool<{ title: string }> {
+const wikipediaToolParams = [
+ {
+ name: 'title',
+ type: 'string',
+ description: 'The title of the Wikipedia article to search',
+ required: true,
+ },
+] as const;
+
+type WikipediaToolParamsType = typeof wikipediaToolParams;
+
+export class WikipediaTool extends BaseTool<WikipediaToolParamsType> {
private _addLinkedUrlDoc: (url: string, id: string) => void;
+
constructor(addLinkedUrlDoc: (url: string, id: string) => void) {
super(
'wikipedia',
'Search Wikipedia and return a summary',
- {
- title: {
- type: 'string',
- description: 'The title of the Wikipedia article to search',
- required: true,
- },
- },
+ wikipediaToolParams,
'Provide simply the title you want to search on Wikipedia and nothing more. If re-using this tool, try a different title for different information.',
'Returns a summary from searching an article title on Wikipedia'
);
this._addLinkedUrlDoc = addLinkedUrlDoc;
}
- async execute(args: { title: string }): Promise<unknown> {
+ async execute(args: ParametersType<WikipediaToolParamsType>): Promise<Observation[]> {
try {
const { text } = await Networking.PostToServer('/getWikipediaSummary', { title: args.title });
const id = uuidv4();
const url = `https://en.wikipedia.org/wiki/${args.title.replace(/ /g, '_')}`;
this._addLinkedUrlDoc(url, id);
- return [{ type: 'text', text: `<chunk chunk_id=${id} chunk_type=csv}> ${text} </chunk>` }];
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${id}" chunk_type="text"> ${text} </chunk>`,
+ },
+ ];
} catch (error) {
console.log(error);
return [{ type: 'text', text: 'An error occurred while fetching the article.' }];
diff --git a/src/client/views/nodes/chatbot/types/types.ts b/src/client/views/nodes/chatbot/types/types.ts
index 2bc7f4952..7abad85f0 100644
--- a/src/client/views/nodes/chatbot/types/types.ts
+++ b/src/client/views/nodes/chatbot/types/types.ts
@@ -1,3 +1,5 @@
+import { AnyLayer } from 'react-map-gl';
+
export enum ASSISTANT_ROLE {
USER = 'user',
ASSISTANT = 'assistant',
@@ -112,17 +114,9 @@ export interface AI_Document {
type: string;
}
-export interface Tool<T extends Record<string, unknown> = Record<string, unknown>> {
- name: string;
- description: string;
- parameters: Record<string, unknown>;
- citationRules: string;
- briefSummary: string;
- execute: (args: T) => Promise<unknown>;
- getActionRule: () => Record<string, unknown>;
-}
-
export interface AgentMessage {
role: 'system' | 'user' | 'assistant';
- content: string | { type: string; text?: string; image_url?: { url: string } }[];
+ content: string | Observation[];
}
+
+export type Observation = { type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } };
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index 0304ddc86..967f4aa5b 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -68,7 +68,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
expand && this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
try {
this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, (this.props.getPos() ?? 0) + (expand ? 2 : 1))));
- } catch (err) {
+ } catch {
/* empty */
}
}, 0);
@@ -95,7 +95,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
setTimeout(() => {
try {
this.props.view.dispatch(state.tr.setSelection(TextSelection.create(state.tr.doc, this.props.getPos() + 2)));
- } catch (err) {
+ } catch {
/* empty */
}
}, 0);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 18b8c9d34..905c69bb8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -65,6 +65,7 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
import { RichTextRules } from './RichTextRules';
import { schema } from './schema_rts';
import { Property } from 'csstype';
+import { LabelBox } from '../LabelBox';
// import * as applyDevTools from 'prosemirror-dev-tools';
export interface FormattedTextBoxProps extends FieldViewProps {
@@ -134,10 +135,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
@observable _showSidebar = false;
- @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 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
set _recordingDictation(value) {
!this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined);
@@ -158,6 +161,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
@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
+ @computed get isLabel() { return this.dataDoc[this.fieldKey+"_fitBox"]; } // prettier-ignore
constructor(props: FormattedTextBoxProps) {
super(props);
@@ -165,7 +169,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this._recordingStart = Date.now();
}
- public get EditorView() { return this._editorView; } // prettier-ignore
+ public get EditorView() { return this.isLabel ? undefined : this._editorView; } // prettier-ignore
// public makeAIFlashcards: () => void = unimplementedFunction;
public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
@@ -175,10 +179,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
// but since removing one anchor from the list of attr anchors isn't implemented, this will end up removing nothing.
public RemoveLinkFromDoc(linkDoc?: Doc) {
this.unhighlightSearchTerms();
- const state = this._editorView?.state;
+ const state = this.EditorView?.state;
const a1 = DocCast(linkDoc?.link_anchor_1);
const a2 = DocCast(linkDoc?.link_anchor_2);
- if (state && a1 && a2 && this._editorView) {
+ if (state && a1 && a2 && this.EditorView) {
this.removeDocument(a1);
this.removeDocument(a2);
let allFoundLinkAnchors: { href: string; title: string; anchorId: string }[] = [];
@@ -188,7 +192,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return true;
});
if (allFoundLinkAnchors.length) {
- this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allAnchors: allFoundLinkAnchors }));
+ this.EditorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allAnchors: allFoundLinkAnchors }));
this.setupEditor(this.config, this.fieldKey);
}
@@ -197,16 +201,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
// removes all the specified link references from the selection.
// NOTE: as above, this won't work correctly if there are marks with overlapping but not exact sets of link references.
public RemoveAnchorFromSelection(allAnchors: { href: string; title: string; linkId: string; targetId: string }[]) {
- const state = this._editorView?.state;
- if (state && this._editorView) {
- this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors }));
+ const state = this.EditorView?.state;
+ if (state && this.EditorView) {
+ this.EditorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors }));
this.setupEditor(this.config, this.fieldKey);
}
}
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;
+ if (!pinProps && this.EditorView?.state.selection.empty) return rootDoc;
const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc });
this.addDocument(anchor);
this._finishingLink = true;
@@ -263,7 +267,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
});
};
- AnchorMenu.Instance.Highlight = undoable((color: string) => this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text');
+ AnchorMenu.Instance.Highlight = undoable((color: string) => this.EditorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text');
AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true);
AnchorMenu.Instance.StartCropDrag = unimplementedFunction;
/**
@@ -292,7 +296,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? '');
- const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
+ const coordsB = this.EditorView!.coordsAtPos(this.EditorView!.state.selection.to);
this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
let ele: Opt<HTMLDivElement>;
try {
@@ -309,7 +313,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
leafText = (node: Node) => {
- if (node.type === this._editorView?.state.schema.nodes.dashField) {
+ 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 fieldKey = StrCast(node.attrs.fieldKey);
return (
@@ -320,16 +324,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return '';
};
dispatchTransaction = (tx: Transaction) => {
- if (this._editorView && !this._editorView.isDestroyed) {
- const state = this._editorView.state.apply(tx);
- this._editorView.updateState(state);
+ if (this.EditorView && !this.EditorView.isDestroyed) {
+ const state = this.EditorView.state.apply(tx);
+ this.EditorView.updateState(state);
this.tryUpdateDoc(false);
}
};
tryUpdateDoc = (force: boolean) => {
- if (this._editorView) {
- const { state } = this._editorView;
+ if (this.EditorView) {
+ const { state } = this.EditorView;
const { dataDoc } = this;
const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText);
const newJson = JSON.stringify(state.toJSON());
@@ -360,6 +364,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
// if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) {
textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
+ textChange && (dataDoc[this.fieldKey + '_placeholder'] = undefined);
const numstring = NumCast(dataDoc[this.fieldKey], null);
dataDoc[this.fieldKey] =
numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined;
@@ -371,7 +376,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
// if we've deleted all the text in a note driven by a template, then restore the template data
dataDoc[this.fieldKey] = undefined;
- this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(rtField.Data)));
+ this.EditorView.updateState(EditorState.fromJSON(this.config, JSON.parse(rtField.Data)));
ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText });
unchanged = false;
}
@@ -386,7 +391,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (jsonstring) {
const json = JSON.parse(jsonstring);
json.selection = state.toJSON().selection;
- this._editorView.updateState(EditorState.fromJSON(this.config, json));
+ this.EditorView.updateState(EditorState.fromJSON(this.config, json));
}
}
if (window.getSelection()?.isCollapsed && this._props.rootSelected?.()) {
@@ -406,8 +411,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
linkAnchor = anchor;
}
});
- if (this._editorView && linkTime) {
- const { state } = this._editorView;
+ if (this.EditorView && linkTime) {
+ const { state } = this.EditorView;
const node = state.selection.$from.node();
if (linkAnchor && node.type !== state.schema.nodes.code_block) {
const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000;
@@ -415,7 +420,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const { from } = state.selection;
const value = state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] });
const replaced = state.tr.insert(from - 1, value);
- this._editorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1))));
+ this.EditorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1))));
}
}
};
@@ -430,16 +435,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
(Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
link.link_relationship === LinkManager.AutoKeywords
); // prettier-ignore
- if (this._editorView?.state.doc.textContent) {
- let { tr } = this._editorView.state;
- const { from, to } = this._editorView.state.selection;
- const { autoLinkAnchor } = this._editorView.state.schema.marks;
+ if (this.EditorView?.state.doc.textContent) {
+ let { tr } = this.EditorView.state;
+ const { from, to } = this.EditorView.state.selection;
+ const { autoLinkAnchor } = this.EditorView.state.schema.marks;
tr = tr.removeMark(0, tr.doc.content.size, autoLinkAnchor);
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)));
- this._editorView?.dispatch(tr);
+ this.EditorView?.dispatch(tr);
}
oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(doc => Doc.DeleteLink?.(doc));
};
@@ -449,11 +454,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (
!this._props.dontRegisterView && // (this.Document.isTemplateForField === "text" || !this.Document.isTemplateForField) && // only update the title if the data document's data field is changing
title.startsWith('-') &&
- this._editorView &&
+ this.EditorView &&
!this.dataDoc.title_custom &&
(Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text')
) {
- let node = this._editorView.state.doc;
+ let node = this.EditorView.state.doc;
while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild;
const str = node.textContent;
const prefix = '-';
@@ -476,7 +481,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
*/
hyperlinkTerm = (trIn: Transaction, target: Doc, newAutoLinks: Set<Doc>) => {
let tr = trIn;
- const editorView = this._editorView;
+ const editorView = this.EditorView;
if (editorView && !Doc.AreProtosEqual(target, this.Document)) {
const autoLinkTerm = Field.toString(target.title as FieldType).replace(/^@/, '');
let alink: Doc | undefined;
@@ -552,18 +557,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
unhighlightSearchTerms = () => {
- if (this._editorView) {
- const { state } = this._editorView;
+ if (this.EditorView) {
+ const { state } = this.EditorView;
if (state) {
const mark = state.schema.mark(state.schema.marks.search_highlight);
const activeMark = state.schema.mark(state.schema.marks.search_highlight, { selected: true });
const end = state.doc.nodeSize - 2;
- this._editorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
+ this.EditorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
}
}
};
adoptAnnotation = (start: number, end: number, mark: Mark) => {
- const view = this._editorView!;
+ const view = this.EditorView!;
const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail() });
view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
};
@@ -615,7 +620,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (added) {
draggedDoc._freeform_fitContentsToBox = true;
Doc.SetContainer(draggedDoc, this.Document);
- const view = this._editorView!;
+ const view = this.EditorView!;
try {
this._inDrop = true;
const pos = view.posAtCoords({ left: de.x, top: de.y })?.pos;
@@ -809,7 +814,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
let target: Element | HTMLElement | null = e.target as HTMLElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
while (target && (!(target instanceof HTMLElement) || !target.dataset?.targethrefs)) target = target.parentElement;
- const editor = this._editorView;
+ const editor = this.EditorView;
if (editor && target && !(e.nativeEvent instanceof simMouseEvent ? e.nativeEvent.dash : false)) {
const hrefs = (target.dataset?.targethrefs as string)
?.trim()
@@ -995,8 +1000,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const c = this.ProseRef?.getElementsByTagName('img');
if (c) {
for (const i of c) {
- console.log(i);
-
// console.log(canvas.toDataURL());
// canvas.style.zIndex = '2000000';
// document.body.appendChild(canvas);
@@ -1021,8 +1024,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
animateRes = (resIndex: number, newText: string) => {
if (resIndex < newText.length) {
- const marks = this._editorView?.state.storedMarks ?? [];
- this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks));
+ const marks = this.EditorView?.state.storedMarks ?? [];
+ this.EditorView?.dispatch(this.EditorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks));
setTimeout(() => this.animateRes(resIndex + 1, newText), 20);
}
};
@@ -1034,8 +1037,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
if (!res) {
this.animateRes(0, 'Something went wrong.');
- } else if (this._editorView) {
- const { dispatch, state } = this._editorView;
+ } else if (this.EditorView) {
+ const { dispatch, state } = this.EditorView;
// for no animation, use: dispatch(state.tr.insertText(res));
// for animted response starting at end of text, use:
dispatch(state.tr.setSelection(Selection.atEnd(state.doc)));
@@ -1056,13 +1059,13 @@ 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 { 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({})));
+ this.EditorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({})));
if (this._recordingDictation) {
this.recordDictation();
}
@@ -1081,7 +1084,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
stopDictation = (/* abort: boolean */) => DictationManager.Controls.stop(/* !abort */);
setDictationContent = (value: string) => {
- if (this._editorView && this._recordingStart) {
+ if (this.EditorView && this._recordingStart) {
if (this._break) {
const textanchorFunc = () => {
const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' });
@@ -1094,22 +1097,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const textanchor = Cast(link.link_anchor_1, Doc, null);
if (audioanchor) {
audioanchor.backgroundColor = 'tan';
- const audiotag = this._editorView.state.schema.nodes.audiotag.create({
+ const audiotag = this.EditorView.state.schema.nodes.audiotag.create({
timeCode: NumCast(audioanchor._timecodeToShow),
audioId: audioanchor[Id],
textId: textanchor[Id],
});
textanchor[DocData].title = 'dictation:' + audiotag.attrs.timeCode;
- const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag);
+ 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)));
+ this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size)));
}
}
}
- const { from } = this._editorView.state.selection;
+ const { from } = this.EditorView.state.selection;
this._break = false;
- const tr = this._editorView.state.tr.insertText(value);
- this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView());
+ const tr = this.EditorView.state.tr.insertText(value);
+ this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView());
}
};
@@ -1142,7 +1145,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
});
this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents
- this._editorView!.dispatch(tr.removeMark(selection.from, selection.to, splitter));
+ this.EditorView!.dispatch(tr.removeMark(selection.from, selection.to, splitter));
this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false;
anchor.text = selectedText;
anchor.text_html = this._selectionHTML ?? selectedText;
@@ -1155,7 +1158,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return anchorDoc ?? this.Document;
}
- getView = async (doc: Doc, options: FocusViewOptions) => {
+ getView = (doc: Doc, options: FocusViewOptions) => {
if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
if (!this.SidebarShown) {
this.toggleSidebar(false);
@@ -1176,7 +1179,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
let hadStart = start !== 0;
frag.forEach((node, index) => {
const examinedNode = findAnchorNode(node, editor);
- if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) {
+ if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this.EditorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this.EditorView?.state.schema.nodes.audiotag)) {
nodes.push(examinedNode.node);
!hadStart && (start = index + examinedNode.start);
hadStart = true;
@@ -1185,13 +1188,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return { frag: Fragment.fromArray(nodes), start };
};
const findAnchorNode = (node: Node, editor: EditorView) => {
- if (node.type === this._editorView?.state.schema.nodes.audiotag) {
+ if (node.type === this.EditorView?.state.schema.nodes.audiotag) {
if (node.attrs.textId === textAnchorId) {
return { node, start: 0 };
}
return undefined;
}
- if (node.type === this._editorView?.state.schema.nodes.dashDoc) {
+ if (node.type === this.EditorView?.state.schema.nodes.dashDoc) {
if (node.attrs.docId === textAnchorId) {
return { node, start: 0 };
}
@@ -1207,9 +1210,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
this._didScroll = false; // assume we don't need to scroll. if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below
- if (this._editorView && textAnchorId) {
- const { state } = this._editorView;
- const ret = findAnchorFrag(state.doc.content, this._editorView);
+ if (this.EditorView && textAnchorId) {
+ const { state } = this.EditorView;
+ const ret = findAnchorFrag(state.doc.content, this.EditorView);
const firstChild = ret.frag.childCount ? ret.frag.child(0) : undefined;
if (ret.start >= 0 && (ret.frag.size || (firstChild && [state.schema.nodes.dashDoc, state.schema.nodes.audioTag].includes(firstChild.type)))) {
@@ -1218,7 +1221,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (ret.frag.firstChild) {
selection = TextSelection.between(state.doc.resolve(ret.start), state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
}
- this._editorView.dispatch(state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
+ this.EditorView.dispatch(state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId;
addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' });
setTimeout(() => {
@@ -1306,15 +1309,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
},
incomingValue => {
- if (this._editorView && this._applyingChange !== this.fieldKey) {
+ if (this.EditorView && this._applyingChange !== this.fieldKey) {
if (incomingValue?.data) {
const updatedState = JSON.parse(incomingValue.data.Data);
- if (JSON.stringify(this._editorView.state.toJSON()) !== JSON.stringify(updatedState)) {
- this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
+ 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 ?? '')));
+ } else if (this.EditorView.state.doc.textContent !== (incomingValue?.str ?? '')) {
+ selectAll(this.EditorView.state, tx => this.EditorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
}
}
},
@@ -1328,18 +1331,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
);
this._disposers.selected = reaction(
- () => this._props.rootSelected?.(),
+ () => this._props.rootSelected?.() || this._props.isContentActive(),
action(selected => {
+ if (selected && this.dataDoc[this.fieldKey + '_placeholder']) {
+ setTimeout(() => {
+ selectAll(this.EditorView!.state, (tx: Transaction) => {
+ this.EditorView?.dispatch(tx);
+ this.EditorView!.focus();
+ });
+ });
+ }
this.prepareForTyping();
if (FormattedTextBox._globalHighlights.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 && !selected) {
+ if (((RichTextMenu.Instance?.view === this.EditorView && this.EditorView) || this.isLabel) && !selected) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
}
- if (this._editorView && selected) {
- RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc);
- setTimeout(this.autoLink, 20);
+ if (selected) {
+ RichTextMenu.Instance?.updateMenu(this.EditorView, undefined, this._props, this.dataDoc);
+ this.EditorView && setTimeout(this.autoLink, 20);
}
}),
{ fireImmediately: true }
@@ -1380,7 +1391,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
// DocCast(this.Document.image)._freeform_fitContentsToBox = true;
// Doc.SetContainer(DocCast(this.Document.image), this.Document);
- // const view = this._editorView!;
+ // const view = this.EditorView!;
// try {
// this._inDrop = true;
// const pos = view.posAtCoords({ left: 0, top: 0 })?.pos;
@@ -1427,7 +1438,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
};
addPdfReference = (pdfAnchorId: string) => {
- const view = this._editorView!;
+ const view = this.EditorView!;
if (pdfAnchorId) {
DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => {
if (pdfAnchor instanceof Doc) {
@@ -1478,7 +1489,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
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();
+ this.EditorView?.destroy();
this._editorView = new EditorView(this.ProseRef, {
state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config),
handleScrollToSelection: editorView => {
@@ -1506,20 +1517,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
- const { state, dispatch } = this._editorView;
+ const { state } = this._editorView;
if (!rtfField) {
const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
const startupText = Field.toString(dataDoc[fieldKey] as FieldType);
- if (startupText) {
- dispatch(state.tr.insertText(startupText));
- }
- const textAlign = StrCast(this.dataDoc.text_align, StrCast(Doc.UserDoc().textAlign, 'left'));
+ const textAlign = StrCast(this.dataDoc[this.fieldKey + '_align'], StrCast(Doc.UserDoc().textAlign)) || 'left';
if (textAlign !== 'left') {
selectAll(this._editorView.state, tr => {
- this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign })));
+ this.EditorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign })));
});
- this.tryUpdateDoc(true);
}
+ if (startupText) {
+ this.EditorView?.dispatch(this.EditorView.state.tr.insertText(startupText));
+ }
+ this.tryUpdateDoc(true);
}
this._editorView.TextView = this;
}
@@ -1530,56 +1541,57 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
Doc.SetSelectOnLoad(undefined);
FormattedTextBox.SelectOnLoadChar = '';
}
- if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
+ if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
this._props.select(false);
if (selLoadChar) {
- 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.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
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 curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
- const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks);
- const tr2 = 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);
+ const tr1 = this.EditorView.state.tr.setStoredMarks(storedMarks);
+ const tr2 = 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);
const tr = tr2.setStoredMarks(storedMarks);
- this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
+ this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
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
} else if (!FormattedTextBox.DontSelectInitialText) {
const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
- selectAll(this._editorView.state, (tx: Transaction) => {
- this._editorView?.dispatch(tx.addStoredMark(mark));
+ selectAll(this.EditorView.state, (tx: Transaction) => {
+ this.EditorView?.dispatch(tx.addStoredMark(mark));
});
+ this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(new TextSelection(this.EditorView.state.doc.resolve(1))));
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
} else {
- 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.anchor ? this.EditorView.state.doc.resolve(this.EditorView.state.selection.anchor - 1) : undefined;
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 curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
- const { tr } = this._editorView.state;
- this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks));
+ const { tr } = this.EditorView.state;
+ this.EditorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).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
}
}
if (selectOnLoad) {
FormattedTextBox.DontSelectInitialText = false;
- this._editorView!.focus();
+ this.EditorView!.focus();
}
if (this._props.isContentActive()) this.prepareForTyping();
- if (this._editorView && FormattedTextBox.PasteOnLoad) {
+ if (this.EditorView && FormattedTextBox.PasteOnLoad) {
const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor');
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.
+ if (this._props.autoFocus) setTimeout(() => this.EditorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it.
}
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
prepareForTyping = () => {
- if (this._editorView) {
+ if (this.EditorView) {
const { text, paragraph } = schema.nodes;
- const selNode = this._editorView.state.selection.$anchor.node();
- if (this._editorView.state.selection.from === 1 && this._editorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
+ const selNode = this.EditorView.state.selection.$anchor.node();
+ if (this.EditorView.state.selection.from === 1 && this.EditorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })];
- this._editorView.state.selection.empty && this._editorView.state.selection.from === 1 && this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor));
+ this.EditorView.state.selection.empty && this.EditorView.state.selection.from === 1 && this.EditorView?.dispatch(this.EditorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor));
}
}
};
@@ -1593,7 +1605,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
FormattedTextBox.LiveTextUndo?.end();
FormattedTextBox.LiveTextUndo = undefined;
this.unhighlightSearchTerms();
- this._editorView?.destroy();
+ this.EditorView?.destroy();
RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined);
FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = 'none');
}
@@ -1672,26 +1684,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
};
setFocus = (ipos?: number) => {
- const pos = ipos ?? (this._editorView?.state.selection.$from.pos || 1);
- setTimeout(() => this._editorView?.dispatch(this._editorView.state.tr.setSelection(TextSelection.near(this._editorView.state.doc.resolve(pos)))), 100);
+ const pos = ipos ?? (this.EditorView?.state.selection.$from.pos || 1);
+ setTimeout(() => this.EditorView?.dispatch(this.EditorView.state.tr.setSelection(TextSelection.near(this.EditorView.state.doc.resolve(pos)))), 100);
setTimeout(() => (this.ProseRef?.children?.[0] as HTMLElement).focus(), 200);
};
@action
onFocused = (e: React.FocusEvent): void => {
- // applyDevTools.applyDevTools(this._editorView);
+ // applyDevTools.applyDevTools(this.EditorView);
e.stopPropagation();
};
onClick = (e: React.MouseEvent): void => {
if (!this._props.isContentActive()) return;
- const editorView = this._editorView;
+ const editorView = this.EditorView;
const editorRoot = editorView?.root instanceof Document ? editorView.root : undefined;
if (editorView && (!this._forceUncollapse || editorRoot?.getSelection()?.isCollapsed)) {
// this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
const pcords = editorView.posAtCoords({ left: e.clientX, top: e.clientY });
const node = pcords && editorView.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
if (pcords && node?.type === editorView.state.schema.nodes.dashComment) {
- this._editorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2)));
+ this.EditorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2)));
e.preventDefault();
}
if (!node && this.ProseRef) {
@@ -1717,33 +1729,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, selectOrderedList: boolean = false) {
this._forceUncollapse = false;
clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
- const clickPos = this._editorView!.posAtCoords({ left: x, top: y });
+ const clickPos = this.EditorView!.posAtCoords({ left: x, top: y });
const clickPosVal = clickPos?.pos || 1;
let olistPos = clickPosVal;
if (clickPos && olistPos && this._props.rootSelected?.()) {
- const clickNode = this._editorView?.state.doc.resolve(olistPos).node();
- const nodeBef = this._editorView?.state.doc.resolve(Math.max(0, olistPos - 1)).node();
- olistPos = nodeBef?.type === this._editorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos;
- let $olistPos = this._editorView?.state.doc.resolve(olistPos);
- let olistNode = (nodeBef !== null || clickNode?.type === this._editorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef;
- if (olistNode?.type === this._editorView?.state.schema.nodes.list_item) {
+ const clickNode = this.EditorView?.state.doc.resolve(olistPos).node();
+ const nodeBef = this.EditorView?.state.doc.resolve(Math.max(0, olistPos - 1)).node();
+ olistPos = nodeBef?.type === this.EditorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos;
+ let $olistPos = this.EditorView?.state.doc.resolve(olistPos);
+ let olistNode = (nodeBef !== null || clickNode?.type === this.EditorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef;
+ if (olistNode?.type === this.EditorView?.state.schema.nodes.list_item) {
if ($olistPos && $olistPos.depth) {
olistNode = $olistPos.parent;
- $olistPos = this._editorView?.state.doc.resolve($olistPos.start($olistPos.depth - 1));
+ $olistPos = this.EditorView?.state.doc.resolve($olistPos.start($olistPos.depth - 1));
}
}
- const maxSize = this._editorView?.state.doc.content.size ?? 0;
- const listPos = this._editorView?.state.doc.resolve(Math.min(maxSize, clickPosVal === olistPos ? clickPosVal + 1 : clickPosVal));
+ const maxSize = this.EditorView?.state.doc.content.size ?? 0;
+ const listPos = this.EditorView?.state.doc.resolve(Math.min(maxSize, clickPosVal === olistPos ? clickPosVal + 1 : clickPosVal));
const listNode = listPos?.node();
- if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list && listNode) {
+ if (olistNode && olistNode.type === this.EditorView?.state.schema.nodes.ordered_list && listNode) {
if (!highlightOnly) {
if (selectOrderedList) {
- this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!)));
+ this.EditorView.dispatch(this.EditorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!)));
} else {
const nodePos = clickPosVal - (olistPos === clickPosVal ? 0 : 1);
- if (this._editorView.state.doc.nodeAt(nodePos)) {
- const tr = this._editorView.state.tr.setNodeMarkup(nodePos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility });
- this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, nodePos)));
+ if (this.EditorView.state.doc.nodeAt(nodePos)) {
+ const tr = this.EditorView.state.tr.setNodeMarkup(nodePos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility });
+ this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, nodePos)));
}
}
}
@@ -1763,19 +1775,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
onBlur = (e: React.FocusEvent) => {
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();
+ const stordMarks = this.EditorView?.state.storedMarks?.slice();
if (!(this.EditorView?.state.selection instanceof NodeSelection)) {
this.autoLink();
- if (this._editorView?.state.tr) {
+ if (this.EditorView?.state.tr) {
const tr = stordMarks?.reduce((tr2, m) => {
tr2.addStoredMark(m);
return tr2;
- }, this._editorView.state.tr);
- tr && this._editorView.dispatch(tr);
+ }, this.EditorView.state.tr);
+ tr && this.EditorView.dispatch(tr);
}
}
}
- if (RichTextMenu.Instance?.view === this._editorView && !this._props.rootSelected?.()) {
+ if (RichTextMenu.Instance?.view === this.EditorView && !(this._props.isContentActive() || this._props.rootSelected?.())) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
}
@@ -1822,7 +1834,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
switch (e.key) {
case 'Escape':
- this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
+ this.EditorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
(document.activeElement as HTMLElement).blur?.();
DocumentView.DeselectAll();
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
@@ -1848,7 +1860,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this.startUndoTypingBatch();
};
ondrop = (e: React.DragEvent) => {
- this._editorView!.dispatch(updateBullets(this._editorView!.state.tr, this._editorView!.state.schema));
+ this.EditorView?.dispatch(updateBullets(this.EditorView.state.tr, this.EditorView.state.schema));
e.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash.
};
onScroll = (e: React.UIEvent) => {
@@ -1863,7 +1875,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
tryUpdateScrollHeight = () => {
const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
- if (children && !SnappingManager.IsDragging) {
+ if (this.EditorView && children && !SnappingManager.IsDragging) {
const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
const toNum = (val: string) => Number(val.replace('px', ''));
const toHgt = (node: Element): number => {
@@ -2084,7 +2096,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
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 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 styleFromLayout?.height === '0px' ? null : (
+ return this.isLabel ? (
+ <LabelBox {...this._props} />
+ ) : styleFromLayout?.height === '0px' ? null : (
<div
className="formattedTextBox"
ref={r => {
@@ -2107,6 +2121,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
fontSize: this.fontSize,
fontFamily: this.fontFamily,
fontWeight: this.fontWeight,
+ fontStyle: this.fontStyle,
+ textDecoration: this.fontDecoration,
...styleFromLayout,
}}>
<div
@@ -2138,7 +2154,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
onScroll={this.onScroll}
onDrop={this.ondrop}>
<div
- className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered ? 'centered' : ''} ${this.layoutDoc.hCentering}`}
+ 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}`}
ref={this.createDropTarget}
style={{
padding: StrCast(this.layoutDoc._textBoxPadding),
@@ -2147,8 +2163,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
color: StrCast(this.layoutDoc.text_fontColor),
- fontWeight: `${this.layoutDoc.contentBold ? 'bold' : ''}`,
- textTransform: `${this.layoutDoc.textTransform}` as Property.TextTransform,
+ fontWeight: this.layoutDoc.contentBold ? 'bold' : '',
+ textTransform: StrCast(this.dataDoc[this.fieldKey + '_transform']) as Property.TextTransform,
}}
/>
</div>
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 7a8b72be0..3c84e5a10 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -349,7 +349,9 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa
dispatch(tx4);
}
- if (view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) {
+ 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 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);
}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 88e2e4248..62fbd23ea 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -1,6 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { lift, toggleMark, wrapIn } from 'prosemirror-commands';
import { Mark, MarkType } from 'prosemirror-model';
@@ -32,7 +32,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
private _linkToRef = React.createRef<HTMLInputElement>();
- layoutDoc: Doc | undefined;
+ dataDoc: Doc | undefined;
@observable public view?: EditorView & { TextView?: FormattedTextBox } = undefined;
public editorProps: FieldViewProps | AntimodeMenuProps | undefined;
@@ -41,7 +41,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private collapsed: boolean = false;
@observable private _noLinkActive: boolean = false;
@observable private _boldActive: boolean = false;
- @observable private _italicsActive: boolean = false;
+ @observable private _italicActive: boolean = false;
@observable private _underlineActive: boolean = false;
@observable private _strikethroughActive: boolean = false;
@observable private _subscriptActive: boolean = false;
@@ -49,6 +49,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private _activeFontSize: string = '13px';
@observable private _activeFontFamily: string = '';
+ @observable private _activeFitBox: boolean = false;
@observable private _activeListType: string = '';
@observable private _activeAlignment: string = 'left';
@@ -64,18 +65,21 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private currentLink: string | undefined = '';
@observable private showLinkDropdown: boolean = false;
- _reaction: IReactionDisposer | undefined;
constructor(props: AntimodeMenuProps) {
super(props);
makeObservable(this);
runInAction(() => {
RichTextMenu._instance.menu = this;
- this.updateMenu(undefined, undefined, props, this.layoutDoc);
+ this.updateMenu(undefined, undefined, props, this.dataDoc);
this._canFade = false;
this.Pinned = true;
});
}
+ @computed get RootSelected() {
+ return this.TextView?._props.rootSelected?.() || this.TextView?._props.isContentActive();
+ }
+
@computed get noAutoLink() {
return this._noLinkActive;
}
@@ -85,8 +89,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@computed get underline() {
return this._underlineActive;
}
- @computed get italics() {
- return this._italicsActive;
+ @computed get italic() {
+ return this._italicActive;
}
@computed get strikeThrough() {
return this._strikethroughActive;
@@ -97,6 +101,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@computed get fontHighlight() {
return this._activeHighlightColor;
}
+ @computed get fitBox() {
+ return this._activeFitBox;
+ }
@computed get fontFamily() {
return this._activeFontFamily;
}
@@ -110,26 +117,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return this._activeAlignment;
}
@computed get textVcenter() {
- return BoolCast(this.layoutDoc?._layout_centered);
- }
- _disposer: IReactionDisposer | undefined;
- componentDidMount() {
- // this._disposer = reaction(
- // () => DocumentView.Selected().slice(),
- // () => this.updateMenu(undefined, undefined, undefined, undefined)
- // );
- }
- componentWillUnmount() {
- this._disposer?.();
+ return BoolCast(this.dataDoc?._layout_centered, BoolCast(Doc.UserDoc().layout_centered));
}
@action
- public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: FormattedTextBoxProps | AntimodeMenuProps | undefined, layoutDoc: Doc | undefined) {
+ public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: FormattedTextBoxProps | AntimodeMenuProps | undefined, dataDoc: Doc | undefined) {
if (this._linkToRef.current?.getBoundingClientRect().width) {
return;
}
this.view = view;
- this.layoutDoc = layoutDoc;
+ this.dataDoc = dataDoc;
props && (this.editorProps = props);
// Don't do anything if the document/selection didn't change
@@ -143,12 +140,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const { activeSizes } = active;
const { activeColors } = active;
const { activeHighlights } = active;
- const refDoc = DocumentView.Selected().lastElement()?.layoutDoc ?? Doc.UserDoc();
+ const refDoc = DocumentView.Selected().lastElement()?.dataDoc ?? Doc.UserDoc();
const refField = (pfx => (pfx ? pfx + '_' : ''))(DocumentView.Selected().lastElement()?.LayoutFieldKey);
const refVal = (field: string, dflt: string) => StrCast(refDoc[refField + field], StrCast(Doc.UserDoc()[field], dflt));
this._activeListType = this.getActiveListStyle();
this._activeAlignment = this.getActiveAlignment();
+ this._activeFitBox = BoolCast(refDoc[refField + 'fitBox'], BoolCast(Doc.UserDoc().fitBox));
this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, refVal('fontFamily', 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, refVal('fontSize', '10px')) : activeSizes[0];
this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, refVal('fontColor', 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...';
@@ -177,13 +175,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
toggleMark(mark.type, mark.attrs)(state, dispatch);
}
}
- // this.updateMenu(this.view, undefined, undefined, this.layoutDoc);
}
};
// finds font sizes and families in selection
getActiveAlignment = () => {
- if (this.view && this.TextView?._props.rootSelected?.()) {
+ if (this.view && this.RootSelected) {
const from = this.view.state.selection.$from;
for (let i = from.depth; i >= 0; i--) {
const node = from.node(i);
@@ -191,8 +188,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return node.attrs.align || 'left';
}
}
+ } else if (this.dataDoc) {
+ return StrCast(this.dataDoc.text_align) || 'left';
}
- return 'left';
+ return StrCast(Doc.UserDoc().textAlign) || 'left';
};
// finds font sizes and families in selection
@@ -216,7 +215,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const activeSizes = new Set<string>();
const activeColors = new Set<string>();
const activeHighlights = new Set<string>();
- if (this.view && this.TextView?._props.rootSelected?.()) {
+ if (this.view && this.RootSelected) {
const { state } = this.view;
const pos = this.view.state.selection.$from;
let marks: Mark[] = [...(state.storedMarks ?? [])];
@@ -252,7 +251,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
// finds all active marks on selection in given group
getActiveMarksOnSelection() {
- if (!this.view || !this.TextView?._props.rootSelected?.()) return [] as MarkType[];
+ if (!this.view || !this.RootSelected) return [] as MarkType[];
const { state } = this.view;
let marks: Mark[] = [...(state.storedMarks ?? [])];
@@ -281,7 +280,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this._noLinkActive = false;
this._boldActive = false;
- this._italicsActive = false;
+ this._italicActive = false;
this._underlineActive = false;
this._strikethroughActive = false;
this._subscriptActive = false;
@@ -291,7 +290,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
switch (mark.name) {
case 'noAutoLinkAnchor': this._noLinkActive = true; break;
case 'strong': this._boldActive = true; break;
- case 'em': this._italicsActive = true; break;
+ case 'em': this._italicActive = true; break;
case 'underline': this._underlineActive = true; break;
case 'strikethrough': this._strikethroughActive = true; break;
case 'subscript': this._subscriptActive = true; break;
@@ -326,6 +325,17 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.view.focus();
}
};
+ toggleFitBox = () => {
+ if (this.dataDoc) {
+ const doc = this.dataDoc;
+ (document.activeElement as HTMLElement)?.blur();
+ doc.text_fitBox = !doc.text_fitBox;
+ } else {
+ Doc.UserDoc().fitBox = !Doc.UserDoc().fitBox;
+ Doc.UserDoc().textAlign = Doc.UserDoc().fitBox ? 'center' : undefined;
+ }
+ this.updateMenu(undefined, undefined, undefined, this.dataDoc);
+ };
toggleBold = () => {
if (this.view) {
const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong);
@@ -342,7 +352,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
};
- toggleItalics = () => {
+ toggleItalic = () => {
if (this.view) {
const mark = this.view.state.schema.mark(this.view.state.schema.marks.em);
this.setMark(mark, this.view.state, this.view.dispatch, false);
@@ -350,8 +360,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
};
- setFontField = (value: string, fontField: 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
- if (this.TextView && this.view) {
+ setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => {
+ if (this.TextView && this.view && fontField !== 'fitBox') {
const { text, paragraph } = this.view.state.schema.nodes;
const selNode = this.view.state.selection.$anchor.node();
if (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
@@ -363,9 +373,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs);
this.setMark(fmark, this.view.state, (tx: Transaction) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
this.view.focus();
+ } else if (this.dataDoc) {
+ this.dataDoc[`text_${fontField}`] = value;
+ this.updateMenu(undefined, undefined, undefined, this.dataDoc);
} else {
Doc.UserDoc()[fontField] = value;
- // this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
+ this.updateMenu(undefined, undefined, undefined, this.dataDoc);
}
};
@@ -391,7 +404,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.view!.dispatch(tx3);
});
this.view.focus();
- // this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
};
insertSummarizer(state: EditorState, dispatch: (tr: Transaction) => void) {
@@ -406,10 +418,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
vcenterToggle = () => {
- this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered);
+ if (this.dataDoc) this.dataDoc._layout_centered = !this.dataDoc._layout_centered;
+ else Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered;
};
- align = (view: EditorView, dispatch: (tr: Transaction) => void, alignment: 'left' | 'right' | 'center') => {
- if (this.TextView?._props.rootSelected?.()) {
+ align = (view: EditorView | undefined, dispatch: undefined | ((tr: Transaction) => void), alignment: 'left' | 'right' | 'center') => {
+ if (view && dispatch && this.RootSelected) {
let { tr } = view.state;
view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos) => {
if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) {
@@ -421,6 +434,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
});
view.focus();
dispatch?.(tr);
+ } else {
+ if (this.dataDoc) {
+ this.dataDoc.text_align = alignment;
+ } else Doc.UserDoc().textAlign = alignment;
+ this.updateMenu(undefined, undefined, undefined, this.dataDoc);
}
};
@@ -698,7 +716,7 @@ interface RichTextMenuPluginProps {
}
export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> {
update(view: EditorView & { TextView?: FormattedTextBox }, lastState: EditorState | undefined) {
- RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.layoutDoc);
+ RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.dataDoc);
}
render() {
return null;
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index f58434906..3d14ce4c0 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -121,7 +121,7 @@ export class RichTextRules {
annotationOn: textDoc,
_layout_fitWidth: true,
_layout_autoHeight: true,
- _text_fontSize: '9px',
+ text_fontSize: '9px',
title: 'inline comment',
});
textDocInline.title = inlineFieldKey; // give the annotation its own title
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 7448fa898..06869717a 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -14,7 +14,7 @@ import { StopEvent, lightOrDark, returnFalse, returnOne, setupMoveUpEvents } fro
import { emptyFunction, stringHash } from '../../../../Utils';
import { Doc, DocListCast, Field, FieldResult, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
import { Animation, DocData, TransitionTimer } from '../../../../fields/DocSymbols';
-import { Copy } from '../../../../fields/FieldSymbols';
+import { Copy, Id } from '../../../../fields/FieldSymbols';
import { InkField } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { ObjectField } from '../../../../fields/ObjectField';
@@ -48,6 +48,7 @@ import './PresBox.scss';
import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
import SlideEffect from './SlideEffect';
import { AnimationSettings, SpringSettings, SpringType, easeItems, effectItems, effectTimings, movementItems, presEffectDefaultTimings, springMappings, springPreviewColors } from './SpringUtils';
+import _ from 'lodash';
@observer
export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -381,7 +382,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (typeof res === 'string') {
const resObj = JSON.parse(res);
console.log('Parsed GPT Result ', resObj);
- // eslint-disable-next-line no-restricted-syntax
for (const key in resObj) {
if (resObj[key]) {
console.log('typeof property', typeof resObj[key]);
@@ -562,11 +562,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const datarange = [DocumentType.FUNCPLOT].includes(targetType);
const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
- const typeCollection = targetType === DocumentType.COL;
+ const collectionType = targetType === DocumentType.COL;
const filters = true;
const pivot = true;
const dataannos = false;
- return { scrollable, pannable, inkable, type_collection: typeCollection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
+ return { scrollable, pannable, inkable, collectionType, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
}
@action
@@ -574,7 +574,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/* empty */
};
@action
- // eslint-disable-next-line default-param-last
static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: dataTypes, targetDoc?: Doc) {
const bestTarget = bestTargetView?.Document ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
if (!bestTarget) return undefined;
@@ -700,15 +699,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
changed = true;
}
}
- if ((pinDataTypes?.type_collection && activeItem.config_viewType !== undefined) || (!pinDataTypes && activeItem.config_viewType !== undefined)) {
- if (bestTarget._type_collection !== activeItem.config_viewType) {
- bestTarget._type_collection = activeItem.config_viewType;
+ if ((pinDataTypes?.collectionType && activeItem.config_card_curDoc !== undefined) || (!pinDataTypes && activeItem.config_card_curDoc !== undefined)) {
+ if (bestTarget._card_curDoc !== activeItem.config_card_curDoc) {
+ bestTarget._card_curDoc = activeItem.config_card_curDoc;
+ changed = true;
+ }
+ }
+ if ((pinDataTypes?.collectionType && activeItem.config_carousel_index !== undefined) || (!pinDataTypes && activeItem.config_carousel_index !== undefined)) {
+ if (bestTarget._carousel_index !== activeItem.config_carousel_index) {
+ bestTarget._carousel_index = activeItem.config_carousel_index;
+ changed = true;
+ }
+ }
+ if ((pinDataTypes?.collectionType && activeItem.config_type_collection !== undefined) || (!pinDataTypes && activeItem.config_type_collection !== undefined)) {
+ if (bestTarget._type_collection !== activeItem.config_type_collection) {
+ bestTarget._type_collection = activeItem.config_type_collection;
changed = true;
}
}
if ((pinDataTypes?.filters && activeItem.config_docFilters !== undefined) || (!pinDataTypes && activeItem.config_docFilters !== undefined)) {
- if (bestTarget.childFilters !== activeItem.config_docFilters) {
+ if (!_.isEqual(Array.from(StrListCast(bestTarget.childFilters)), Array.from(StrListCast(activeItem.config_docFilters)))) {
bestTarget.childFilters = ObjectField.MakeCopy(activeItem.config_docFilters as ObjectField) || new List<string>([]);
changed = true;
}
@@ -1134,7 +1145,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return false;
}
} else if (doc.type !== DocumentType.PRES) {
- // eslint-disable-next-line operator-assignment
if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide';
doc.presentation_targetDoc = doc.createdFrom ?? doc; // dropped document will be a new embedding of an embedded document somewhere else.
doc.presentation_movement = PresMovement.Zoom;
@@ -1166,8 +1176,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const tagDoc = Cast(curDoc.presentation_targetDoc, Doc, null);
if (curDoc && curDoc === this.activeItem)
return (
- // eslint-disable-next-line react/no-array-index-key
- <div key={index} className="selectedList-items">
+ <div key={doc[Id]} className="selectedList-items">
<b>
{index + 1}. {StrCast(curDoc.title)})
</b>
@@ -1175,15 +1184,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
if (tagDoc)
return (
- // eslint-disable-next-line react/no-array-index-key
- <div key={index} className="selectedList-items">
+ <div key={doc[Id]} className="selectedList-items">
{index + 1}. {StrCast(curDoc.title)}
</div>
);
if (curDoc)
return (
- // eslint-disable-next-line react/no-array-index-key
- <div key={index} className="selectedList-items">
+ <div key={doc[Id]} className="selectedList-items">
{index + 1}. {StrCast(curDoc.title)}
</div>
);
@@ -1368,7 +1375,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const tagDoc = PresBox.targetRenderedDoc(doc);
const srcContext = Cast(tagDoc.embedContainer, Doc, null);
const labelCreator = (top: number, left: number, edge: number, fontSize: number) => (
- // eslint-disable-next-line react/no-array-index-key
<div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top, left, width: edge, height: edge, fontSize }} onClick={() => this.selectElement(doc)}>
<div className="pathOrder-frame">{index + 1}</div>
</div>
@@ -1619,7 +1625,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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);
- // eslint-disable-next-line camelcase
const { presentation_transition: pt, presentation_duration: pd, presentation_hideBefore: ph, presentation_hideAfter: pa } = this.activeItem;
array.forEach(curDoc => {
curDoc.presentation_transition = pt;
@@ -2017,7 +2022,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="presBox-effects">
{this.generatedAnimations.map((elem, i) => (
<div
- // eslint-disable-next-line react/no-array-index-key
key={i}
className="presBox-effect-container"
onClick={() => {
@@ -2612,13 +2616,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
createTemplate = (layout: string, input?: string) => {
const x = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.x) : 0;
const y = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.y) + NumCast(this.targetDoc._height) + 20 : 0;
- const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _text_fontSize: '24pt' });
- const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _text_fontSize: '16pt' });
- const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _text_fontSize: '20pt' });
- const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _text_fontSize: '24pt' });
- const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _text_fontSize: '14pt' });
- const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _text_fontSize: '14pt' });
- const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _text_fontSize: '14pt' });
+ const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, text_fontSize: '24pt' });
+ const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, text_fontSize: '16pt' });
+ const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, text_fontSize: '20pt' });
+ const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, text_fontSize: '24pt' });
+ const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, text_fontSize: '14pt' });
+ const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, text_fontSize: '14pt' });
+ const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, text_fontSize: '14pt' });
// prettier-ignore
switch (layout) {
case 'blank': return Docs.Create.FreeformDocument([], { title: input || 'Blank slide', _width: 400, _height: 225, x, y });
@@ -3045,7 +3049,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="Slide">
{mode !== CollectionViewType.Invalid ? (
<CollectionView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
PanelWidth={this._props.PanelWidth}
PanelHeight={this.panelHeight}
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 7243473e0..5ab9b556c 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -10,7 +10,6 @@ import { emptyFunction, unimplementedFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
-import { Docs } from '../../documents/Documents';
import { SettingsManager } from '../../util/SettingsManager';
import { undoBatch } from '../../util/UndoManager';
import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
@@ -19,6 +18,7 @@ import { DocumentView } from '../nodes/DocumentView';
import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
import './AnchorMenu.scss';
import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
+import { ComparisonBox } from '../nodes/ComparisonBox';
@observer
export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -117,29 +117,15 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
*/
transferToFlashcard = (text: string, x: number, y: number) => {
- // put each question generated by GPT on the front of the flashcard
- const senArr = text.trim().split('Question:');
- const collectionArr: Doc[] = [];
- for (let i = 1; i < senArr.length; i++) {
- const newDoc = Docs.Create.ComparisonDocument(senArr[i], { _layout_isFlashcard: true, _width: 300, _height: 300 });
- newDoc.text = senArr[i];
-
- collectionArr.push(newDoc);
- }
- // create a new carousel collection of these flashcards
- const newCol = Docs.Create.CarouselDocument(collectionArr, {
- _width: 250,
- _height: 200,
- _layout_fitWidth: false,
- _layout_autoHeight: true,
- });
-
- newCol.x = x;
- newCol.y = y;
- newCol.zIndex = 1000;
-
- this.addToCollection?.(newCol);
- this._loading = false;
+ ComparisonBox.createFlashcardDeck(text, 250, 200, 'data_front', 'data_back').then(
+ action(newCol => {
+ newCol.x = x;
+ newCol.y = y;
+ newCol.zIndex = 1000;
+ this.addToCollection?.(newCol);
+ this._loading = false;
+ })
+ );
};
/**
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index d5f5f620c..a7e78bcea 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -572,18 +572,19 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
<div style={{ height: '80%' }}>
{this.heading(this.mode === GPTPopupMode.SORT ? 'SORTING' : 'QUIZ')}
<>
- {!this.cardsDoneLoading ? (
- <div className="content-wrapper">
- <div className="loading-spinner">
- <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
- {this.loading ? <span>Loading...</span> : <span>Reading Cards...</span>}
+ {
+ !this.cardsDoneLoading ? (
+ <div className="content-wrapper">
+ <div className="loading-spinner">
+ <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
+ {this.loading ? <span>Loading...</span> : <span>Reading Cards...</span>}
+ </div>
</div>
- </div>
- ) : this.mode === GPTPopupMode.CARD ? (
- this.cardMenu()
- ) : (
- this.cardActual(this.mode)
- ) // Call the functions to render JSX
+ ) : this.mode === GPTPopupMode.CARD ? (
+ this.cardMenu()
+ ) : (
+ this.cardActual(this.mode)
+ ) // Call the functions to render JSX
}
</>
</div>
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 358557ad7..920c9ea8b 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -377,7 +377,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
if ((e.button !== 0 || e.altKey) && this._props.isContentActive()) {
this._setPreviewCursor?.(e.clientX, e.clientY, true, false, this._props.Document);
}
- if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
this._props.select(false);
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this.isAnnotating = true;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 0078dfc76..4c5be047b 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -21,7 +21,7 @@ import { SVGToBezier, SVGType } from '../../util/bezierFit';
import { InkingStroke } from '../InkingStroke';
import { ObservableReactComponent } from '../ObservableReactComponent';
import { MarqueeView } from '../collections/collectionFreeForm';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
+import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
import './SmartDrawHandler.scss';
export interface DrawingOptions {
@@ -105,14 +105,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
y: bounds.top - inkWidth / 2,
_width: bounds.width + inkWidth,
_height: bounds.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
inkWidth,
opts.autoColor ? stroke[1] : ActiveInkColor(),
ActiveInkBezierApprox(),
- stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
+ stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2],
+ ActiveInkArrowStart(),
+ ActiveInkArrowEnd(),
+ ActiveInkDash(),
ActiveIsInkMask()
);
drawing.push(inkDoc);
diff --git a/src/extensions/ExtensionsTypings.ts b/src/extensions/ExtensionsTypings.ts
index d6ffd3be3..fa8851bb3 100644
--- a/src/extensions/ExtensionsTypings.ts
+++ b/src/extensions/ExtensionsTypings.ts
@@ -1,6 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
interface Array<T> {
+ /**
+ * returns the last element of the array or undefined
+ */
lastElement(): T;
+ /**
+ * if val is in the list, it returns its index, otherwise undefined;
+ * @param val
+ */
+ getIndex(val: T): number | undefined;
}
interface String {
diff --git a/src/extensions/Extensions_Array.ts b/src/extensions/Extensions_Array.ts
index a50fb330f..d61585e28 100644
--- a/src/extensions/Extensions_Array.ts
+++ b/src/extensions/Extensions_Array.ts
@@ -1,14 +1,13 @@
export default class ArrayExtension {
private readonly property: string;
- private readonly body: <T>(this: Array<T>) => any;
+ private readonly body: <T>(this: Array<T>, args: unknown) => unknown;
- constructor(property: string, body: <T>(this: Array<T>) => any) {
+ constructor(property: string, body: <T>(this: Array<T>, args: unknown) => unknown) {
this.property = property;
this.body = body;
}
assign() {
- // eslint-disable-next-line no-extend-native
Object.defineProperty(Array.prototype, this.property, {
value: this.body,
enumerable: false,
@@ -28,6 +27,11 @@ const extensions = [
}
return this[this.length - 1];
}),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ new ArrayExtension('getIndex', function (val: any) {
+ const index = this.indexOf(val);
+ return index === -1 ? undefined : index;
+ }),
];
function Assign() {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index c77c2a77d..e62ca4bb8 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -14,7 +14,7 @@ import {
Initializing, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width
} from './DocSymbols'; // prettier-ignore
import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols';
-import { InkTool } from './InkField';
+import { InkEraserTool, InkInkTool, InkTool } from './InkField';
import { List } from './List';
import { ObjectField, serverOpType } from './ObjectField';
import { PrefetchProxy, ProxyField } from './Proxy';
@@ -253,6 +253,10 @@ export class Doc extends RefField {
public static set ActivePage(val) { Doc.UserDoc().activePage = val; } // prettier-ignore
public static get ActiveTool(): InkTool { return StrCast(Doc.UserDoc().activeTool, InkTool.None) as InkTool; } // prettier-ignore
public static set ActiveTool(tool:InkTool){ Doc.UserDoc().activeTool = tool; } // prettier-ignore
+ public static get ActiveInk(): InkInkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkInkTool; } // prettier-ignore
+ public static set ActiveInk(tool:InkInkTool){ Doc.UserDoc().activeInkTool = tool; } // prettier-ignore
+ public static get ActiveEraser(): InkEraserTool { return StrCast(Doc.UserDoc().activeEraserTool, InkTool.None) as InkEraserTool; } // prettier-ignore
+ public static set ActiveEraser(tool:InkEraserTool){ Doc.UserDoc().activeEraserTool = tool; } // prettier-ignore
public static get ActivePresentation() { return DocCast(Doc.ActiveDashboard?.activePresentation) as Opt<Doc>; } // prettier-ignore
public static set ActivePresentation(val) { Doc.ActiveDashboard && (Doc.ActiveDashboard.activePresentation = val) } // prettier-ignore
public static get ActiveDashboard() { return DocCast(Doc.UserDoc().activeDashboard); } // prettier-ignore
@@ -960,6 +964,19 @@ export namespace Doc {
}
} else if (field instanceof PrefetchProxy) {
Doc.FindReferences(field.value, references, system);
+ } else if (field instanceof RichTextField) {
+ const re = /"docId"\s*:\s*"(.*?)"/g;
+ let match: string[] | null;
+ while ((match = re.exec(field.Data)) !== null) {
+ const urlString = match[1];
+ if (urlString) {
+ const rdoc = DocServer.GetCachedRefField(urlString);
+ if (rdoc) {
+ references.add(rdoc);
+ Doc.FindReferences(rdoc, references, system);
+ }
+ }
+ }
}
} else if (field instanceof Promise) {
// eslint-disable-next-line no-debugger
@@ -990,7 +1007,7 @@ export namespace Doc {
} else if (field instanceof ObjectField) {
const docAtKey = doc[key];
copy[key] =
- docAtKey instanceof Doc && key.includes('layout[')
+ docAtKey instanceof Doc && (key.includes('layout[') || docAtKey.cloneOnCopy)
? new ProxyField(Doc.MakeCopy(docAtKey)) // copy the expanded render template
: ObjectField.MakeCopy(field);
} else if (field instanceof Promise) {
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 17b99b033..f3ff5f11f 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -8,17 +8,22 @@ import { ObjectField } from './ObjectField';
// Helps keep track of the current ink tool in use.
export enum InkTool {
- None = 'none',
- Pen = 'pen',
- Highlighter = 'highlighter',
- StrokeEraser = 'strokeeraser',
- SegmentEraser = 'segmenteraser',
- RadiusEraser = 'radiuseraser',
- Eraser = 'eraser', // not a real tool, but a class of tools
- Stamp = 'stamp',
- Write = 'write',
- PresentationPin = 'presentationpin',
- SmartDraw = 'smartdraw',
+ None = 'None',
+ Ink = 'Ink',
+ Eraser = 'Eraser', // not a real tool, but a class of tools
+ SmartDraw = 'Smartdraw',
+}
+
+export enum InkInkTool {
+ Pen = 'Pen',
+ Highlight = 'Highlight',
+ Write = 'Write',
+}
+
+export enum InkEraserTool {
+ Stroke = 'Stroke',
+ Segment = 'Segment',
+ Radius = 'Radius',
}
export type Segment = Array<Bezier>;
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
index 613bb0fd1..dc636031a 100644
--- a/src/fields/RichTextField.ts
+++ b/src/fields/RichTextField.ts
@@ -48,4 +48,27 @@ export class RichTextField extends ObjectField {
''
);
}
+
+ public static textToRtf(text: string, imgDocId?: string) {
+ return new RichTextField(
+ JSON.stringify({
+ // this is a RichText json that has the question text placed above a related image
+ doc: {
+ type: 'doc',
+ content: [
+ {
+ type: 'paragraph',
+ attrs: { align: 'center', color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+ content: [
+ ...(text ? [{ type: 'text', text }] : []), //
+ ...(imgDocId ? [{ type: 'dashDoc', attrs: { width: '200px', height: '200px', title: 'dashDoc', float: 'unset', hidden: false, docId: imgDocId } }] : []),
+ ],
+ },
+ ],
+ },
+ selection: { type: 'text', anchor: 2, head: 2 },
+ }),
+ text
+ );
+ }
}
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 582c09f29..0252961d3 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -85,6 +85,7 @@ async function deserializeScript(scriptIn: ScriptField) {
}
@scriptingGlobal
+// eslint-disable-next-line no-use-before-define
@Deserializable('script', (obj: unknown) => deserializeScript(obj as ScriptField))
export class ScriptField extends ObjectField {
@serializable
@@ -137,7 +138,6 @@ export class ScriptField extends ObjectField {
[ToString]() {
return this.script.originalScript;
}
- // eslint-disable-next-line default-param-last
public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }, transformer?: Transformer) {
return CompileScript(script, {
params: {
@@ -156,13 +156,11 @@ export class ScriptField extends ObjectField {
});
}
- // eslint-disable-next-line default-param-last
public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
return compiled.compiled ? new ScriptField(compiled) : undefined;
}
- // eslint-disable-next-line default-param-last
public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
const compiled = ScriptField.CompileScript(script, params, false, capturedVariables);
return compiled.compiled ? new ScriptField(compiled) : undefined;
@@ -186,6 +184,7 @@ export class ScriptField extends ObjectField {
}
@scriptingGlobal
+// eslint-disable-next-line no-use-before-define
@Deserializable('computed', (obj: unknown) => deserializeScript(obj as ComputedField))
export class ComputedField extends ScriptField {
static undefined = '__undefined';
@@ -229,7 +228,6 @@ export class ComputedField extends ScriptField {
[ToValue](doc: Doc) { return ComputedField.useComputed ? { value: this.value(doc) } : undefined; } // prettier-ignore
[Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } // prettier-ignore
- // eslint-disable-next-line default-param-last
public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) {
const compiled = ScriptField.CompileScript(script, params, true, { value: '', ...capturedVariables });
const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined;
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 335683270..b27816f55 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -48,8 +48,8 @@ export const documentSchema = createSchema({
_columnsHideIfEmpty: 'boolean', // whether empty stacking view column headings should be hidden
// _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry
// _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views
- _text_fontSize: 'string',
- _text_fontFamily: 'string',
+ text_fontSize: 'string',
+ text_fontFamily: 'string',
_layout_sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar
// appearance properties on the data document
@@ -70,7 +70,7 @@ export const documentSchema = createSchema({
stroke_startMarker: 'string',
stroke_endMarker: 'string',
stroke_dash: 'string',
- textTransform: 'string',
+ text_transform: 'string',
treeView_Open: 'boolean', // flag denoting whether the documents sub-tree (contents) is visible or hidden
treeView_ExpandedView: 'string', // name of field whose contents are being displayed as the document's subtree
treeView_ExpandedViewLock: 'boolean', // whether the expanded view can be changed
@@ -112,5 +112,4 @@ export const collectionSchema = createSchema({
});
export type Document = makeInterface<[typeof documentSchema]>;
-// eslint-disable-next-line no-redeclare
export const Document = makeInterface(documentSchema);
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 60eadcdfd..33764aca5 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -227,7 +227,6 @@ function getEffectiveAcl(target: Doc | ListImpl<FieldType>, user?: string): symb
* @param allowUpgrade whether permissions can be made less restrictive
* @param layoutOnly just sets the layout doc's ACL (unless the data doc has no entry for the ACL, in which case it will be set as well)
*/
-// eslint-disable-next-line default-param-last
export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited: Doc[] = [], allowUpgrade?: boolean, layoutOnly = false) {
const selfKey = `acl_${normalizeEmail(ClientUtils.CurrentUserEmail())}`;
if (!target || visited.includes(target) || key === selfKey) return;
diff --git a/src/pen-gestures/GestureTypes.ts b/src/pen-gestures/GestureTypes.ts
index 5a8e9bd97..10f9ba6d0 100644
--- a/src/pen-gestures/GestureTypes.ts
+++ b/src/pen-gestures/GestureTypes.ts
@@ -8,7 +8,6 @@ export enum Gestures {
Arrow = 'arrow',
RightAngle = 'rightangle',
}
-
// Defines a point in an ink as a pair of x- and y-coordinates.
export interface PointData {
X: number;
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index 04262b61f..f6e1c87fa 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -1,4 +1,5 @@
/* eslint-disable no-use-before-define */
+import { numberRange } from '../Utils';
import { Gestures } from './GestureTypes';
/**
@@ -193,77 +194,38 @@ export class NDollarRecognizer {
constructor(
useBoundedRotationInvariance: boolean // constructor
) {
+ const rectMaker = (width: number, height1: number, height2: number) => [
+ new Point(0, 0), //
+ new Point(0, height1),
+ new Point(width, height2),
+ new Point(width, 0),
+ new Point(0, 0),
+ ];
+
+ const arect = rectMaker(100, 100, 50);
+ const aorect = rectMaker(300, 100, 50);
+ const brect = rectMaker(100, 100, 200);
+ const borect = rectMaker(300, 100, 200);
+ const rect = rectMaker(100, 100, 100);
+ const orect = rectMaker(300, 100, 100);
+ const equilateral = [new Point(50, 100), new Point(100, 0), new Point(0, 0), new Point(50, 100)];
+ const aequilateral = [new Point(20, 100), new Point(200, 0), new Point(0, 0), new Point(20, 100)];
+ const bequilateral = [new Point(180, 100), new Point(200, 0), new Point(0, 0), new Point(180, 100)];
+ const circle = numberRange(11).map(i => new Point(100 + 100 * Math.cos((i / 10) * Math.PI * 2), 100 + 100 * Math.sin((i / 10) * Math.PI * 2)));
+ const rightAngle = [new Point(0, 0), new Point(0, 100), new Point(200, 100)];
//
- // one predefined multistroke for each multistroke type
+ // one predefined multistroke (plus its counterclockwise reversal for closed shapes) for each multistroke type
//
this.Multistrokes.push(
- new Multistroke(
- Gestures.Rectangle,
- useBoundedRotationInvariance,
- new Array([
- new Point(30, 146), // new Point(29, 160), new Point(30, 180), new Point(31, 200),
- new Point(30, 222), // new Point(50, 219), new Point(70, 225), new Point(90, 230),
- new Point(106, 225), // new Point(100, 200), new Point(106, 180), new Point(110, 160),
- new Point(106, 146), // new Point(80, 150), new Point(50, 146),
- new Point(30, 143),
- ])
- )
- );
- this.Multistrokes.push(new Multistroke(Gestures.Rectangle, useBoundedRotationInvariance, new Array([new Point(30, 143), new Point(106, 146), new Point(106, 225), new Point(30, 222), new Point(30, 146)])));
- this.Multistrokes.push(new Multistroke(Gestures.Line, useBoundedRotationInvariance, [[new Point(12, 347), new Point(119, 347)]]));
- this.Multistrokes.push(
- new Multistroke(
- Gestures.Triangle, // equilateral
- useBoundedRotationInvariance,
- new Array([new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100)])
- )
- );
- this.Multistrokes.push(
- new Multistroke(
- Gestures.Triangle, // equilateral
- useBoundedRotationInvariance,
- new Array([new Point(42, 100), new Point(140, 102), new Point(100, 200), new Point(40, 100)])
- )
- );
- this.Multistrokes.push(
- new Multistroke(
- Gestures.Circle,
- useBoundedRotationInvariance,
- new Array([
- new Point(200, 250),
- new Point(240, 230),
- new Point(248, 210),
- new Point(248, 190),
- new Point(240, 170),
- new Point(200, 150),
- new Point(160, 170),
- new Point(151, 190),
- new Point(151, 210),
- new Point(160, 230),
- new Point(201, 250),
- ])
- )
- );
- this.Multistrokes.push(
- new Multistroke(
- Gestures.Circle,
- useBoundedRotationInvariance,
- new Array([
- new Point(201, 250),
- new Point(160, 230),
- new Point(151, 210),
- new Point(151, 190),
- new Point(160, 170),
- new Point(200, 150),
- new Point(240, 170),
- new Point(248, 190),
- new Point(248, 210),
- new Point(240, 230),
- new Point(200, 250),
- ])
- )
+ ...[arect, aorect, brect, borect, rect, orect].map(s => new Multistroke(Gestures.Rectangle, useBoundedRotationInvariance, [s])),
+ ...[arect, aorect, brect, borect, rect, orect].map(s => new Multistroke(Gestures.Rectangle, useBoundedRotationInvariance, [s.reverse()])),
+ ...[aequilateral, bequilateral, equilateral].map(s => new Multistroke(Gestures.Triangle, useBoundedRotationInvariance, [s])),
+ ...[aequilateral, bequilateral, equilateral].map(s => new Multistroke(Gestures.Triangle, useBoundedRotationInvariance, [s.reverse()])),
+ new Multistroke(Gestures.Circle, useBoundedRotationInvariance, [circle]),
+ new Multistroke(Gestures.Circle, useBoundedRotationInvariance, [circle.reverse()]),
+ new Multistroke(Gestures.RightAngle, useBoundedRotationInvariance, [rightAngle]),
+ new Multistroke(Gestures.Line, useBoundedRotationInvariance, [[new Point(12, 347), new Point(119, 347)]])
);
- this.Multistrokes.push(new Multistroke(Gestures.RightAngle, useBoundedRotationInvariance, new Array([new Point(0, 0), new Point(0, 100), new Point(200, 100)])));
NumMultistrokes = this.Multistrokes.length; // NumMultistrokes flags the end of the non user-defined gstures strokes
//
// PREDEFINED STROKES
diff --git a/src/server/ApiManagers/AssistantManager.ts b/src/server/ApiManagers/AssistantManager.ts
index b7d4191ca..8447a4934 100644
--- a/src/server/ApiManagers/AssistantManager.ts
+++ b/src/server/ApiManagers/AssistantManager.ts
@@ -15,7 +15,6 @@ import * as fs from 'fs';
import { writeFile } from 'fs';
import { google } from 'googleapis';
import { JSDOM } from 'jsdom';
-import OpenAI from 'openai';
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import { promisify } from 'util';
@@ -307,33 +306,35 @@ export default class AssistantManager extends ApiManager {
// If the result contains image or table chunks, save the base64 data as image files
if (result.chunks && Array.isArray(result.chunks)) {
- for (const chunk of result.chunks) {
- if (chunk.metadata && (chunk.metadata.type === 'image' || chunk.metadata.type === 'table')) {
- const files_directory = '/files/chunk_images/';
- const directory = path.join(publicDirectory, files_directory);
-
- // Ensure the directory exists or create it
- if (!fs.existsSync(directory)) {
- fs.mkdirSync(directory);
+ await Promise.all(
+ result.chunks.map(chunk => {
+ if (chunk.metadata && (chunk.metadata.type === 'image' || chunk.metadata.type === 'table')) {
+ const files_directory = '/files/chunk_images/';
+ const directory = path.join(publicDirectory, files_directory);
+
+ // Ensure the directory exists or create it
+ if (!fs.existsSync(directory)) {
+ fs.mkdirSync(directory);
+ }
+
+ const fileName = path.basename(chunk.metadata.file_path); // Get the file name from the path
+ const filePath = path.join(directory, fileName); // Create the full file path
+
+ // Check if the chunk contains base64 encoded data
+ if (chunk.metadata.base64_data) {
+ // Decode the base64 data and write it to a file
+ const buffer = Buffer.from(chunk.metadata.base64_data, 'base64');
+ fs.promises.writeFile(filePath, buffer).then(() => {
+ // Update the file path in the chunk's metadata
+ chunk.metadata.file_path = path.join(files_directory, fileName);
+ chunk.metadata.base64_data = undefined; // Remove the base64 data from the metadata
+ });
+ } else {
+ console.warn(`No base64_data found for chunk: ${fileName}`);
+ }
}
-
- const fileName = path.basename(chunk.metadata.file_path); // Get the file name from the path
- const filePath = path.join(directory, fileName); // Create the full file path
-
- // Check if the chunk contains base64 encoded data
- if (chunk.metadata.base64_data) {
- // Decode the base64 data and write it to a file
- const buffer = Buffer.from(chunk.metadata.base64_data, 'base64');
- await fs.promises.writeFile(filePath, buffer);
-
- // Update the file path in the chunk's metadata
- chunk.metadata.file_path = path.join(files_directory, fileName);
- chunk.metadata.base64_data = undefined; // Remove the base64 data from the metadata
- } else {
- console.warn(`No base64_data found for chunk: ${fileName}`);
- }
- }
- }
+ })
+ );
result.status = 'completed';
} else {
result.status = 'pending';
@@ -355,39 +356,42 @@ export default class AssistantManager extends ApiManager {
// Initialize an array to hold the formatted content
const content: { type: string; text?: string; image_url?: { url: string } }[] = [{ type: 'text', text: '<chunks>' }];
- for (const chunk of relevantChunks) {
- // Format each chunk by adding its metadata and content
- content.push({
- type: 'text',
- text: `<chunk chunk_id=${chunk.id} chunk_type="${chunk.metadata.type}">`,
- });
-
- // If the chunk is an image or table, read the corresponding file and encode it as base64
- if (chunk.metadata.type === 'image' || chunk.metadata.type === 'table') {
- try {
- const filePath = serverPathToFile(Directory.chunk_images, chunk.metadata.file_path); // Get the file path
- const imageBuffer = await readFileAsync(filePath); // Read the image file
- const base64Image = imageBuffer.toString('base64'); // Convert the image to base64
-
- // Add the base64-encoded image to the content array
- if (base64Image) {
- content.push({
- type: 'image_url',
- image_url: {
- url: `data:image/jpeg;base64,${base64Image}`,
- },
+ await Promise.all(
+ relevantChunks.map((chunk: { id: string; metadata: { type: string; text: TimeRanges; file_path: string } }) => {
+ // Format each chunk by adding its metadata and content
+ content.push({
+ type: 'text',
+ text: `<chunk chunk_id=${chunk.id} chunk_type="${chunk.metadata.type}">`,
+ });
+
+ // If the chunk is an image or table, read the corresponding file and encode it as base64
+ if (chunk.metadata.type === 'image' || chunk.metadata.type === 'table') {
+ try {
+ const filePath = serverPathToFile(Directory.chunk_images, chunk.metadata.file_path); // Get the file path
+ readFileAsync(filePath).then(imageBuffer => {
+ const base64Image = imageBuffer.toString('base64'); // Convert the image to base64
+
+ // Add the base64-encoded image to the content array
+ if (base64Image) {
+ content.push({
+ type: 'image_url',
+ image_url: {
+ url: `data:image/jpeg;base64,${base64Image}`,
+ },
+ });
+ } else {
+ console.log(`Failed to encode image for chunk ${chunk.id}`);
+ }
});
- } else {
- console.log(`Failed to encode image for chunk ${chunk.id}`);
+ } catch (error) {
+ console.error(`Error reading image file for chunk ${chunk.id}:`, error);
}
- } catch (error) {
- console.error(`Error reading image file for chunk ${chunk.id}:`, error);
}
- }
- // Add the chunk's text content to the formatted content
- content.push({ type: 'text', text: `${chunk.metadata.text}\n</chunk>\n` });
- }
+ // Add the chunk's text content to the formatted content
+ content.push({ type: 'text', text: `${chunk.metadata.text}\n</chunk>\n` });
+ })
+ );
content.push({ type: 'text', text: '</chunks>' });
diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts
index 041f65592..74e8c288a 100644
--- a/src/server/GarbageCollector.ts
+++ b/src/server/GarbageCollector.ts
@@ -1,7 +1,4 @@
/* eslint-disable no-await-in-loop */
-/* eslint-disable no-continue */
-/* eslint-disable no-cond-assign */
-/* eslint-disable no-restricted-syntax */
import * as fs from 'fs';
import * as path from 'path';
import { Database } from './database';
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 0cf9a6e58..4dcb32f8b 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -130,7 +130,7 @@ function proxyServe(req: any, requrl: string, response: any) {
const htmlText = htmlInputText
.toString('utf8')
.replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>')
- .replace(/(src|href)=(['"])(https?[^\2\n]*)\1/g, refToCors) // replace src or href='http(s)://...' or href="http(s)://.."
+ .replace(/(src|href)=(['"])(https?[^\n]*)\1/g, refToCors) // replace src or href='http(s)://...' or href="http(s)://.."
// .replace(/= *"\/([^"]*)"/g, relpathToCors)
.replace(/data-srcset="[^"]*"/g, '')
.replace(/srcset="[^"]*"/g, '')