diff options
114 files changed, 2431 insertions, 2345 deletions
diff --git a/package-lock.json b/package-lock.json index 3b11eda7c..9acca90ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@pinecone-database/pinecone": "^2.2.2", "@react-google-maps/api": "^2.19.2", "@react-spring/web": "^9.7.5", + "@thednp/position-observer": "^1.0.7", "@turf/turf": "^7.1.0", "@types/bezier-js": "^4.1.3", "@types/brotli": "^1.3.4", @@ -12081,6 +12082,29 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@thednp/position-observer": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@thednp/position-observer/-/position-observer-1.0.7.tgz", + "integrity": "sha512-MkUAMMgqZPxy71hpcrKr9ZtedMk+oIFbFs5B8uKD857iuYKRJxgJtC1Itus14EEM4qMyeN0x47AUZJmZJQyXbQ==", + "license": "MIT", + "dependencies": { + "@thednp/shorty": "^2.0.10" + }, + "engines": { + "node": ">=16", + "pnpm": ">=8.6.0" + } + }, + "node_modules/@thednp/shorty": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@thednp/shorty/-/shorty-2.0.10.tgz", + "integrity": "sha512-H+hs1lw3Yc1NfwG0b7F7YmVjxQZ31NO2+6zx+I+9XabHxdwPKjvYJnkKKXr7bSItgm2AFrfOn5+3veB6W4iauw==", + "license": "MIT", + "engines": { + "node": ">=16", + "pnpm": ">=8.6.0" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", diff --git a/package.json b/package.json index fe76189bf..1038c470e 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "@pinecone-database/pinecone": "^2.2.2", "@react-google-maps/api": "^2.19.2", "@react-spring/web": "^9.7.5", + "@thednp/position-observer": "^1.0.7", "@turf/turf": "^7.1.0", "@types/bezier-js": "^4.1.3", "@types/brotli": "^1.3.4", diff --git a/packages/components/src/components/Button/Button.scss b/packages/components/src/components/Button/Button.scss index bbe2e2470..c86db9fad 100644 --- a/packages/components/src/components/Button/Button.scss +++ b/packages/components/src/components/Button/Button.scss @@ -53,7 +53,7 @@ &.inactive { &:hover { .background { - filter: opacity(0) !important; + filter: opacity(0); } } } @@ -63,13 +63,16 @@ filter: opacity(0); &.active { - filter: opacity(0.2) !important; + filter: opacity(0.3); } } &:hover { .background { - filter: opacity(0.2); + filter: opacity(0.3); + &.active { + filter: opacity(0); + } } } } @@ -79,7 +82,7 @@ filter: opacity(0); &.active { - filter: opacity(0.2) !important; + filter: opacity(0.2); } } @@ -96,12 +99,12 @@ } .background { - filter: opacity(1) !important; + filter: opacity(1); } &:hover { .background { - filter: brightness(0.8); + filter: opacity(0.3); } } } diff --git a/packages/components/src/components/Button/Button.tsx b/packages/components/src/components/Button/Button.tsx index a91c74a4c..885403640 100644 --- a/packages/components/src/components/Button/Button.tsx +++ b/packages/components/src/components/Button/Button.tsx @@ -1,195 +1,188 @@ -import { Tooltip } from '@mui/material' -import React from 'react' -import { Alignment, IGlobalProps, Placement, Type , getFormLabelSize } from '../../global' -import { Colors, Size } from '../../global/globalEnums' -import { getFontSize, getHeight, isDark } from '../../global/globalUtils' -import { IconButton } from '../IconButton' -import './Button.scss' +import { Tooltip } from '@mui/material'; +import React from 'react'; +import { Alignment, IGlobalProps, Placement, Type, getFormLabelSize } from '../../global'; +import { Colors, Size } from '../../global/globalEnums'; +import { getFontSize, getHeight } from '../../global/globalUtils'; +import { IconButton } from '../IconButton'; +import './Button.scss'; export interface IButtonProps extends IGlobalProps { - onClick?: (event: React.MouseEvent) => void - onDoubleClick?: (event: React.MouseEvent) => void - type?: Type - active?: boolean + onClick?: (event: React.MouseEvent) => void; + onDoubleClick?: (event: React.MouseEvent) => void; + type?: Type; + active?: boolean; - // Content - text?: string - icon?: JSX.Element | string + // Content + text?: string; + icon?: JSX.Element | string; - // Additional stylization - iconPlacement?: Placement - color?: string - colorPicker?: string, - uppercase?: boolean, - align?: Alignment + // Additional stylization + iconPlacement?: Placement; + color?: string; + colorPicker?: string; + uppercase?: boolean; + align?: Alignment; + filter?: string; } export const Button = (props: IButtonProps) => { - const { - text, - icon, - onClick, - onDoubleClick, - onPointerDown, - active, - height, - inactive, - type = Type.PRIM, - label, - uppercase = false, - iconPlacement = 'right', - size = Size.SMALL, - color = Colors.MEDIUM_BLUE, - background, - style, - tooltip, - tooltipPlacement = 'top', - colorPicker, - formLabel, - formLabelPlacement, - fillWidth, - align = fillWidth ? 'flex-start' : 'center' - } = props + const { + text, + icon, + onClick, + onDoubleClick, + onPointerDown, + active, + height, + inactive, + type = Type.PRIM, + filter, + uppercase = false, + iconPlacement = 'right', + size = Size.SMALL, + color = Colors.MEDIUM_BLUE, + background, + style, + tooltip, + tooltipPlacement = 'top', + colorPicker, + formLabel, + formLabelPlacement, + fillWidth, + align = fillWidth ? 'flex-start' : 'center', + } = props; - if (!text) { - return <IconButton {...props}/> - } - - /** - * Pointer down - * @param e - */ - const handlePointerDown = (e: React.PointerEvent) => { - - if (!inactive && onPointerDown) { - e.stopPropagation(); - e.preventDefault(); - onPointerDown(e) + if (!text) { + return <IconButton {...props} />; } - } - /** - * In the event that there is a single click - * @param e - */ - const handleClick = (e: React.MouseEvent) => { - if (!inactive && onClick) { - e.stopPropagation(); - e.preventDefault(); - onClick(e) - } - } + /** + * Pointer down + * @param e + */ + const handlePointerDown = (e: React.PointerEvent) => { + if (!inactive && onPointerDown) { + e.stopPropagation(); + e.preventDefault(); + onPointerDown(e); + } + }; - /** - * Double click - * @param e - */ - const handleDoubleClick = (e: React.MouseEvent) => { - if (!inactive && onDoubleClick){ - e.stopPropagation(); - e.preventDefault(); - onDoubleClick(e) - } - } + /** + * In the event that there is a single click + * @param e + */ + const handleClick = (e: React.MouseEvent) => { + if (!inactive && onClick) { + e.stopPropagation(); + e.preventDefault(); + onClick(e); + } + }; - const getBorderColor = (): Colors | string | undefined => { - switch(type){ - case Type.PRIM: - return undefined; - case Type.SEC: - if (colorPicker) return colorPicker; - return color; - case Type.TERT: - if (colorPicker) return colorPicker; - if (active) return color; - else return color; - } - } + /** + * Double click + * @param e + */ + const handleDoubleClick = (e: React.MouseEvent) => { + if (!inactive && onDoubleClick) { + e.stopPropagation(); + e.preventDefault(); + onDoubleClick(e); + } + }; - const getColor = (): Colors | string | undefined => { - if (color && background) return color; - switch(type){ - case Type.PRIM: - if (colorPicker) return colorPicker - return color; - case Type.SEC: - if (colorPicker) return colorPicker - return color; - case Type.TERT: - if (colorPicker) { - if (isDark(colorPicker)) return Colors.WHITE; - else return Colors.BLACK + const getBorderColor = (): Colors | string | undefined => { + switch (type) { + case Type.PRIM: + return undefined; + case Type.SEC: + if (colorPicker) return colorPicker; + return color; + case Type.TERT: + return color; } - if (isDark(color)) return Colors.WHITE; - else return Colors.BLACK - } - } + }; - const getBackground = (): Colors | string | undefined => { - if (background) return background; - switch(type) { - case Type.PRIM: - if (colorPicker) return colorPicker - return color; - case Type.SEC: - if (colorPicker) return colorPicker - return color; - case Type.TERT: - if (colorPicker) return colorPicker - else return color - } - } + const getColor = (): Colors | string | undefined => { + if (color && background) return color; + switch (type) { + case Type.PRIM: + case Type.SEC: + if (colorPicker) return colorPicker; + return color; + case Type.TERT: + return ''; + } + }; - const defaultProperties: React.CSSProperties = { - height: getHeight(height, size), - minHeight: getHeight(height, size), - width: fillWidth ? '100%' : 'fit-content', - justifyContent: align ? align : undefined, - padding: fillWidth && align === 'center' ? 0 : undefined, - fontWeight: 500, - fontSize: getFontSize(size), - fontFamily: 'sans-serif', - textTransform: uppercase ? 'uppercase' : undefined, - borderColor: getBorderColor(), - color: getColor(), - } + const getBackground = (): Colors | string | undefined => { + if (background) return background; + switch (type) { + case Type.PRIM: + case Type.SEC: + if (colorPicker) return colorPicker; + return color; + case Type.TERT: + } + }; - const backgroundProperties: React.CSSProperties = { - background: getBackground() - } + const defaultProperties: React.CSSProperties = { + height: getHeight(height, size), + minHeight: getHeight(height, size), + width: fillWidth ? '100%' : 'fit-content', + justifyContent: align ? align : undefined, + padding: fillWidth && align === 'center' ? 0 : undefined, + fontWeight: 500, + fontSize: getFontSize(size), + fontFamily: 'sans-serif', + textTransform: uppercase ? 'uppercase' : undefined, + borderColor: getBorderColor(), + color: getColor(), + }; - const button: JSX.Element = ( - <Tooltip disableInteractive={true} arrow={true} placement={tooltipPlacement} title={tooltip}> - <div - className={`button-container ${type} ${active && 'active'} ${inactive && 'inactive'}`} - onClick={handleClick} - onDoubleClick={handleDoubleClick} - onPointerDown={handlePointerDown} - style={{...defaultProperties, ...style}} - > - <div className={`button-content`} - style={{justifyContent: align}} - > - {iconPlacement == 'left' && icon ? <div className={`icon`} style={{ - fontSize: getFontSize(size, true) - }}>{icon}</div> : null} - {text} - {iconPlacement == 'right' && icon ? <div className={`icon`} style={{ - fontSize: getFontSize(size, true) - }}>{icon}</div> : null} - </div> - <div className={`background ${active && 'active'}`} style={backgroundProperties}/> - </div> - </Tooltip> - ) + const backgroundProperties: React.CSSProperties = { + background: getBackground(), + filter, + }; - return ( - formLabel ? - <div className={`form-wrapper ${formLabelPlacement}`} style={{ width: fillWidth ? '100%' : undefined}}> - <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> - {button} - </div> - : - button - ) -} + const button: JSX.Element = ( + <Tooltip disableInteractive={true} arrow={true} placement={tooltipPlacement} title={tooltip}> + <div className={`button-container ${type} ${active && 'active'} ${inactive && 'inactive'}`} onClick={handleClick} onDoubleClick={handleDoubleClick} onPointerDown={handlePointerDown} style={{ ...defaultProperties, ...style }}> + <div className="button-content" style={{ justifyContent: align }}> + {iconPlacement == 'left' && icon ? ( + <div + className="icon" + style={{ + fontSize: getFontSize(size, true), + }}> + {icon} + </div> + ) : null} + {text} + {iconPlacement == 'right' && icon ? ( + <div + className="icon" + style={{ + fontSize: getFontSize(size, true), + }}> + {icon} + </div> + ) : null} + </div> + <div className={`background ${active && 'active'}`} style={backgroundProperties} /> + </div> + </Tooltip> + ); + + return formLabel ? ( + <div className={`form-wrapper ${formLabelPlacement}`} style={{ width: fillWidth ? '100%' : undefined }}> + <div className="formLabel" style={{ fontSize: getFormLabelSize(size) }}> + {formLabel} + </div> + {button} + </div> + ) : ( + button + ); +}; diff --git a/packages/components/src/components/ColorPicker/ColorPicker.tsx b/packages/components/src/components/ColorPicker/ColorPicker.tsx index 632b470f0..72e02d955 100644 --- a/packages/components/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/components/src/components/ColorPicker/ColorPicker.tsx @@ -1,5 +1,5 @@ import React, { useRef, useState } from 'react'; -import { GithubPicker, ChromePicker, BlockPicker, SliderPicker, SketchPicker } from 'react-color'; +import { GithubPicker, ChromePicker, BlockPicker, SliderPicker, SketchPicker, ColorResult } from 'react-color'; import { IGlobalProps, Size, Type, getFormLabelSize } from '../../global'; import { Button } from '../Button'; import { IconButton } from '../IconButton'; @@ -16,12 +16,12 @@ export interface IColorPickerProps extends IGlobalProps { colorPickerType?: ColorPickerType; defaultPickerType?: ColorPickerType; selectedColor?: string; - setSelectedColor: (color: any) => unknown; - setFinalColor: (color: any) => unknown; + setSelectedColor: (color: string) => unknown; + setFinalColor: (color: string) => unknown; } export const ColorPicker = (props: IColorPickerProps) => { - const [selectedColorLoc, setSelectedColorLoc] = useState(); + const [selectedColorLoc, setSelectedColorLoc] = useState<string>(); const { defaultPickerType, text, @@ -31,6 +31,7 @@ export const ColorPicker = (props: IColorPickerProps) => { size = Size.SMALL, type = Type.TERT, icon, + background, selectedColor = selectedColorLoc, setSelectedColor = setSelectedColorLoc, setFinalColor = setSelectedColorLoc, @@ -40,32 +41,25 @@ export const ColorPicker = (props: IColorPickerProps) => { } = props; const [isOpen, setOpen] = useState<boolean>(false); const [pickerSelectorOpen, setPickerSelectorOpen] = useState<boolean>(false); - const decimalToHexString = (number: number) => { + const decimalToHexString = (numberIn: number) => { + let number = numberIn; if (number < 0) { number = 0xffffffff + number + 1; } return (number < 16 ? '0' : '') + number.toString(16).toUpperCase(); }; - const colorString = (color: any) => { - return color.hex === 'transparent' ? color.hex : color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : 'ff'); - }; - const onChange = (color: any) => { - setSelectedColor(colorString(color) as any); - }; - const onChangeComplete = (color: any) => { - setFinalColor(colorString(color) as any); - }; + const colorString = (c: ColorResult) => (c.hex === 'transparent' ? c.hex : c.hex + (c.rgb.a ? decimalToHexString(Math.round(c.rgb.a * 255)) : 'ff')); + const onChange = (c: ColorResult) => setSelectedColor(colorString(c)); + const onChangeComplete = (c: ColorResult) => setFinalColor(colorString(c)); const [picker, setPicker] = useState<string>(defaultPickerType ?? 'Classic'); const toggleRef = useRef<HTMLDivElement | null>(null); const getToggle = () => ( <div ref={toggleRef}> - {icon && !text ? ( - <IconButton active={isOpen} tooltip={tooltip} type={type} color={color} size={size} icon={icon} colorPicker={selectedColor} fillWidth={fillWidth} /> - ) : text ? ( - <Button active={isOpen} tooltip={tooltip} size={size} type={type} color={color} text={text} icon={icon} align={'flex-start'} iconPlacement={'left'} colorPicker={selectedColor} fillWidth={fillWidth} /> + {text && !icon ? ( + <Button active={isOpen} tooltip={tooltip} size={size} type={type} background={background} color={color} text={text} icon={icon} align="flex-start" iconPlacement="left" colorPicker={selectedColor} fillWidth={fillWidth} /> ) : ( - <IconButton active={isOpen} tooltip={tooltip} type={type} color={color} size={size} icon={icon} colorPicker={selectedColor} fillWidth={fillWidth} /> + <IconButton active={isOpen} tooltip={tooltip} type={type} color={color} background={background} size={size} icon={icon} colorPicker={selectedColor} fillWidth={fillWidth} /> )} </div> ); @@ -92,14 +86,12 @@ export const ColorPicker = (props: IColorPickerProps) => { ); } // prettier-ignore }; - const openChanged = (isOpen: boolean) => setPickerSelectorOpen(isOpen); + const openChanged = (state: boolean) => setPickerSelectorOpen(state); const getPopup = (): JSX.Element => { if (colorPickerType) { return getColorPicker(colorPickerType); } else { - // Todo: this would be much easier if the selectedColor was a Color, not a string. - const newColor = selectedColor === 'transparent' ? 'white' : selectedColor?.startsWith('#') ? selectedColor.substring(0, 7) : selectedColor?.startsWith('rgba') ? selectedColor?.replace(/,[0-9]*\)/, '1)') : selectedColor; return ( <div style={{ height: 'fit-content' }}> <Dropdown @@ -109,9 +101,10 @@ export const ColorPicker = (props: IColorPickerProps) => { val: item, }; })} + background={background} activeChanged={openChanged} placement={'right'} - color={newColor} + color={color} type={Type.PRIM} dropdownType={DropdownType.SELECT} selectedVal={picker} @@ -126,7 +119,7 @@ export const ColorPicker = (props: IColorPickerProps) => { const popupContainsPt = (x: number, y: number) => { const rect = toggleRef.current?.getBoundingClientRect(); - return rect && rect.left < x && rect.top < y && rect.right > x && rect.bottom > y; + return pickerSelectorOpen || (rect && rect.left < x && rect.top < y && rect.right > x && rect.bottom > y) ? true : false; }; const colorPicker: JSX.Element = ( @@ -138,6 +131,7 @@ export const ColorPicker = (props: IColorPickerProps) => { tooltip={tooltip} size={size} color={selectedColor} + background={background} popup={getPopup()} popupContainsPt={popupContainsPt} // this should prohbably test to see if the click pt is actually within the picker selector list popup. /> @@ -145,7 +139,7 @@ export const ColorPicker = (props: IColorPickerProps) => { return formLabel ? ( <div className={`form-wrapper ${formLabelPlacement}`} style={{ width: fillWidth ? '100%' : undefined }}> - <div className={'formLabel'} style={{ fontSize: getFormLabelSize(size) }}> + <div className="formLabel" style={{ fontSize: getFormLabelSize(size) }}> {formLabel} </div> {colorPicker} diff --git a/packages/components/src/components/Dropdown/Dropdown.tsx b/packages/components/src/components/Dropdown/Dropdown.tsx index b9b6f01b8..783c7daa4 100644 --- a/packages/components/src/components/Dropdown/Dropdown.tsx +++ b/packages/components/src/components/Dropdown/Dropdown.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { FaCaretDown, FaCaretLeft, FaCaretRight, FaCaretUp } from 'react-icons/fa'; import { Popup, PopupTrigger } from '..'; -import { Colors, IGlobalProps, Placement, Type, getFontSize, getHeight, isDark, getFormLabelSize } from '../../global'; +import { Colors, IGlobalProps, Placement, Type, getFontSize, getHeight, getFormLabelSize } from '../../global'; import { IconButton } from '../IconButton'; import { ListBox } from '../ListBox'; import { IListItemProps, ListItem } from '../ListItem'; @@ -92,7 +92,7 @@ export const Dropdown = (props: IDropdownProps) => { textTransform: uppercase ? 'uppercase' : undefined, borderColor: getBorderColor(), background, - color: color && background ? color : type == Type.TERT ? (isDark(color) ? Colors.WHITE : Colors.BLACK) : color, + color: color, }; const backgroundProperties: React.CSSProperties = { @@ -116,9 +116,9 @@ export const Dropdown = (props: IDropdownProps) => { <div className={`dropdown-toggle${!selectedVal ? '-mini' : ''} ${type} ${inactive && 'inactive'}`} style={{ ...defaultProperties, height: getHeight(height, size), width: width }}> {selectedVal && <ListItem size={size} {...itemsMap.get(selectedVal)} style={{ color: defaultProperties.color, background: defaultProperties.background }} inactive />} <div className="toggle-caret"> - <IconButton size={size} icon={getCaretDirection(active, placement)} color={defaultProperties.color} inactive /> + <IconButton size={size} background={background} icon={getCaretDirection(active, placement)} color={defaultProperties.color} inactive /> </div> - <div className={`background ${active && 'active'}`} style={{ ...backgroundProperties }} /> + <div className={`background ${active && 'active'}`} style={{ ...backgroundProperties, filter: selectedVal ? 'opacity(0.3)' : 'opacity(0)' }} /> </div> ); case DropdownType.CLICK: @@ -127,9 +127,9 @@ export const Dropdown = (props: IDropdownProps) => { <div className={`dropdown-toggle${!selectedVal ? '-mini' : ''} ${type} ${inactive && 'inactive'}`} style={{ ...defaultProperties, height: getHeight(height, size), width: width }}> <ListItem val="title" text={title} size={size} style={{ color: defaultProperties.color, background: defaultProperties.backdropFilter }} inactive /> <div className="toggle-caret"> - <IconButton size={size} icon={getCaretDirection(active, placement)} color={defaultProperties.color} inactive /> + <IconButton size={size} background={background} icon={getCaretDirection(active, placement)} color={defaultProperties.color} inactive /> </div> - <div className={`background ${active && 'active'}`} style={{ ...backgroundProperties }} /> + <div className={`background ${active && 'active'}`} style={{ ...backgroundProperties, filter: selectedVal ? 'opacity(0.3)' : 'opacity(0)' }} /> </div> ); } @@ -157,6 +157,7 @@ export const Dropdown = (props: IDropdownProps) => { size={size} fillWidth={true} color={color} + background={background} popup={ <ListBox maxItems={maxItems} diff --git a/packages/components/src/components/DropdownSearch/DropdownSearch.tsx b/packages/components/src/components/DropdownSearch/DropdownSearch.tsx index 5ec01b44e..bdf114140 100644 --- a/packages/components/src/components/DropdownSearch/DropdownSearch.tsx +++ b/packages/components/src/components/DropdownSearch/DropdownSearch.tsx @@ -1,24 +1,24 @@ -import React, { useState } from 'react' -import * as fa from 'react-icons/fa' -import { EditableText, Popup, PopupTrigger } from '..' -import { IGlobalProps, Placement, Size, getHeight , getFormLabelSize } from '../../global' -import { IconButton } from '../IconButton' -import { ListBox } from '../ListBox' -import { IListItemProps } from '../ListItem' -import './DropdownSearch.scss' +import React, { useState } from 'react'; +import * as fa from 'react-icons/fa'; +import { EditableText, Popup, PopupTrigger } from '..'; +import { IGlobalProps, Placement, Size, getHeight } from '../../global'; +import { IconButton } from '../IconButton'; +import { ListBox } from '../ListBox'; +import { IListItemProps } from '../ListItem'; +import './DropdownSearch.scss'; export enum DropdownSearchType { - SELECT = "select", - CLICK = "click" + SELECT = 'select', + CLICK = 'click', } export interface IDropdownSearchProps extends IGlobalProps { - items: IListItemProps[] - placement: Placement - dropdownSearchType: DropdownSearchType - title?: string - selectedVal?: string | number - maxItems?: number + items: IListItemProps[]; + placement: Placement; + dropdownSearchType: DropdownSearchType; + title?: string; + selectedVal?: string | number; + maxItems?: number; } /** @@ -30,100 +30,76 @@ export interface IDropdownSearchProps extends IGlobalProps { * Look at: import Select from "react-select"; */ export const DropdownSearch = (props: IDropdownSearchProps) => { - const { - size, - height, - maxItems, - items, - dropdownSearchType, - selectedVal, - // setSelectedVal, - tooltip, - title = "DropdownSearch", - type, - width, - color - } = props + const { size, height, maxItems, items, dropdownSearchType, type, width, color } = props; - // const [selectedItem, setSelectedItem] = useState< - // IListItemProps | undefined - // >(selectedVal) + // const [selectedItem, setSelectedItem] = useState< + // IListItemProps | undefined + // >(selectedVal) - const [searchTerm, setSearchTerm] = useState<string | undefined>(undefined) - const [isEditing, setIsEditing] = useState<boolean>(false) - const [active, setActive] = useState<boolean>(false) + const [searchTerm, setSearchTerm] = useState<string | undefined>(undefined); + const [isEditing, setIsEditing] = useState<boolean>(false); + const [active, setActive] = useState<boolean>(false); - const getToggle = () => { - switch (dropdownSearchType) { - case DropdownSearchType.SELECT: - return (<div - className={`dropdownsearch-toggle ${type}`} - style={{ height: getHeight(height, size), width: width }} - onClick={(e) => { - e.stopPropagation() - !isEditing && setIsEditing(true) - }} - > - {/* {selectedItem && !isEditing ? ( + const getToggle = () => { + switch (dropdownSearchType) { + case DropdownSearchType.SELECT: + return ( + <div + className={`dropdownsearch-toggle ${type}`} + style={{ height: getHeight(height, size), width: width }} + onClick={e => { + e.stopPropagation(); + !isEditing && setIsEditing(true); + }}> + {/* {selectedItem && !isEditing ? ( <ListItem {...selectedItem} inactive /> ) : ( */} - <div className="toggle-button"> - <EditableText - type={type} - val={searchTerm} - placeholder={'...'} - editing={true} - // onEdit={(val) => { - // setSearchTerm(val) - // }} - size={Size.SMALL} - setEditing={setIsEditing} - /> - </div> - {/* )} */} - <div className="toggle-caret"> - <IconButton - size={Size.SMALL} - icon={<fa.FaSearch />} - inactive - /> - </div> - <div className={`toggle-background ${isEditing && 'active'}`}/> - </div>); - case DropdownSearchType.CLICK: - default: - return ( - <div - className={`dropdownsearch-toggle ${type}`} - style={{ height: getHeight(height, size), width: width }} - > - </div> - ) - } - } + <div className="toggle-button"> + <EditableText + type={type} + val={searchTerm} + placeholder={'...'} + editing={true} + // onEdit={(val) => { + // setSearchTerm(val) + // }} + size={Size.SMALL} + setEditing={setIsEditing} + /> + </div> + {/* )} */} + <div className="toggle-caret"> + <IconButton size={Size.SMALL} icon={<fa.FaSearch />} inactive /> + </div> + <div className={`toggle-background ${isEditing && 'active'}`} /> + </div> + ); + case DropdownSearchType.CLICK: + default: + return <div className={`dropdownsearch-toggle ${type}`} style={{ height: getHeight(height, size), width: width }}></div>; + } + }; - return ( - <div - className="dropdownsearch-container" - > - <Popup - toggle={getToggle()} - trigger={PopupTrigger.CLICK} - isOpen={active} - setOpen={setActive} - size={size} - color={color} - popup={ - <ListBox - maxItems={maxItems} - items={items} - filter={searchTerm} - // selectedVal={selectedVal} - // setSelectedVal={setSelectedItem} - size={size} - /> - } - /> - </div> - ) -} + return ( + <div className="dropdownsearch-container"> + <Popup + toggle={getToggle()} + trigger={PopupTrigger.CLICK} + isOpen={active} + setOpen={setActive} + size={size} + color={color} + popup={ + <ListBox + maxItems={maxItems} + items={items} + filter={searchTerm} + // selectedVal={selectedVal} + // setSelectedVal={setSelectedItem} + size={size} + /> + } + /> + </div> + ); +}; diff --git a/packages/components/src/components/EditableText/EditableText.tsx b/packages/components/src/components/EditableText/EditableText.tsx index c361cf183..56cbc064d 100644 --- a/packages/components/src/components/EditableText/EditableText.tsx +++ b/packages/components/src/components/EditableText/EditableText.tsx @@ -1,21 +1,21 @@ -import React, { useState } from 'react' -import { Colors, IGlobalProps, Size, TextAlignment, Type, getFontSize, getFormLabelSize, getHeight, isDark } from '../../global' -import './EditableText.scss' -import { Toggle, ToggleType } from '../Toggle' -import { FaEye, FaEyeSlash} from 'react-icons/fa' +import React, { useState } from 'react'; +import { Colors, IGlobalProps, Size, TextAlignment, Type, getFontSize, getFormLabelSize, getHeight, isDark } from '../../global'; +import './EditableText.scss'; +import { Toggle, ToggleType } from '../Toggle'; +import { FaEye, FaEyeSlash } from 'react-icons/fa'; export interface IEditableTextProps extends IGlobalProps { - val?: string | number - setVal?: (newText: string | number) => unknown - onEnter?: (newText: string | number) => unknown - setEditing?: (bool: boolean) => unknown - placeholder?: string - editing?: boolean - size?: Size - height?: number - multiline?: boolean - textAlign?: TextAlignment - password?: boolean + val?: string | number; + setVal?: (newText: string | number) => unknown; + onEnter?: (newText: string | number) => unknown; + setEditing?: (bool: boolean) => unknown; + placeholder?: string; + editing?: boolean; + size?: Size; + height?: number; + multiline?: boolean; + textAlign?: TextAlignment; + password?: boolean; } /** @@ -25,152 +25,149 @@ export interface IEditableTextProps extends IGlobalProps { * @returns */ export const EditableText = (props: IEditableTextProps) => { - const [valLoc, setValLoc] = useState<string>('') - const [editingLoc, setEditingLoc] = useState<boolean>(false) - const { - height, - size, - val = valLoc, - setVal = setValLoc, - onEnter, - setEditing = setEditingLoc, - color = Colors.MEDIUM_BLUE, - background, - type = Type.PRIM, - placeholder, - width, - multiline, - textAlign = 'left', - formLabel, - formLabelPlacement, - fillWidth, - password, - editing = password ? true : editingLoc, - style - } = props - const [showPassword, setShowPassword] = useState<boolean>(false) + const [valLoc, setValLoc] = useState<string>(''); + const [editingLoc, setEditingLoc] = useState<boolean>(false); + const { + height, + size, + val = valLoc, + setVal = setValLoc, + onEnter, + setEditing = setEditingLoc, + color = Colors.MEDIUM_BLUE, + background, + type = Type.PRIM, + placeholder, + width, + textAlign = 'left', + formLabel, + formLabelPlacement, + fillWidth, + password, + editing = password ? true : editingLoc, + style, + } = props; + const [showPassword, setShowPassword] = useState<boolean>(false); - const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => { - setVal(event.target.value) - } - const handleKeyPress = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - onEnter?.((event.target as HTMLInputElement).value) - } - } + const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => { + setVal(event.target.value); + }; + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + onEnter?.((event.target as HTMLInputElement).value); + } + }; - const getBorderColor = (): Colors | string | undefined => { - switch(type){ - case Type.PRIM: - return undefined; - case Type.SEC: - return color; - case Type.TERT: - if (editing) return color; - else return color; - } - } + const getBorderColor = (): Colors | string | undefined => { + switch (type) { + case Type.PRIM: + return undefined; + case Type.SEC: + return color; + case Type.TERT: + if (editing) return color; + else return color; + } + }; - const getColor = (): Colors | string | undefined => { - if (color && background) return color; - switch(type){ - case Type.PRIM: - return color; - case Type.SEC: - return color; - case Type.TERT: - if (isDark(color)) return Colors.WHITE; - else return Colors.BLACK - } - } + const getColor = (): Colors | string | undefined => { + if (color && background) return color; + switch (type) { + case Type.PRIM: + return color; + case Type.SEC: + return color; + case Type.TERT: + if (isDark(color)) return Colors.WHITE; + else return Colors.BLACK; + } + }; - const getBackground = (): Colors | string | undefined => { - if (background) return background; - switch(type){ - case Type.PRIM: - return color; - case Type.SEC: - return color; - case Type.TERT: - return color - } - } + const getBackground = (): Colors | string | undefined => { + if (background) return background; + switch (type) { + case Type.PRIM: + return color; + case Type.SEC: + return color; + case Type.TERT: + return color; + } + }; - const defaultProperties: React.CSSProperties = { - height: getHeight(height, size), - minHeight: getHeight(height, size), - width: fillWidth ? '100%' : width, - padding: undefined, - fontWeight: 500, - fontSize: getFontSize(size), - fontFamily: 'sans-serif', - borderColor: getBorderColor(), - color: getColor() - } + const defaultProperties: React.CSSProperties = { + height: getHeight(height, size), + minHeight: getHeight(height, size), + width: fillWidth ? '100%' : width, + padding: undefined, + fontWeight: 500, + fontSize: getFontSize(size), + fontFamily: 'sans-serif', + borderColor: getBorderColor(), + color: getColor(), + }; - const backgroundProperties: React.CSSProperties = { - background: getBackground() - } + const backgroundProperties: React.CSSProperties = { + background: getBackground(), + }; - const editableText: JSX.Element = ( - <div className={`editableText-container ${type}`} - style={{...defaultProperties, ...style}} - onClick={() => setEditing(true)} - > - {editing ? ( - <input - className={`editableText ${type} ${textAlign}`} - style={{ - height: getHeight(height, size), - textAlign: textAlign, - width: fillWidth ? '100%' : width - }} - placeholder={placeholder} - type={password && !showPassword ? 'password' : undefined} - autoFocus - onChange={handleOnChange} - onKeyPress={handleKeyPress} - onBlur={() => { - !password && setEditing(false) - }} - defaultValue={val} - ></input> - ) : ( - <div - className={`displayText ${type} ${textAlign}`} - style={{ - height: getHeight(height, size), - textAlign: textAlign, - width: fillWidth ? '100%' : width - }} - > - {val ? val : placeholder} + const editableText: JSX.Element = ( + <div className={`editableText-container ${type}`} style={{ ...defaultProperties, ...style }} onClick={() => setEditing(true)}> + {editing ? ( + <input + className={`editableText ${type} ${textAlign}`} + style={{ + height: getHeight(height, size), + textAlign: textAlign, + width: fillWidth ? '100%' : width, + }} + placeholder={placeholder} + type={password && !showPassword ? 'password' : undefined} + autoFocus + onChange={handleOnChange} + onKeyPress={handleKeyPress} + onBlur={() => { + !password && setEditing(false); + }} + defaultValue={val}></input> + ) : ( + <div + className={`displayText ${type} ${textAlign}`} + style={{ + height: getHeight(height, size), + textAlign: textAlign, + width: fillWidth ? '100%' : width, + }}> + {val ? val : placeholder} + </div> + )} + {password && ( + <div className={`password`}> + <Toggle + toggleType={ToggleType.BUTTON} + type={Type.PRIM} + size={size} + color={color} + toggleStatus={showPassword} + onClick={() => setShowPassword(!showPassword)} + tooltip={`${showPassword ? 'Hide' : 'Show'} Password`} + icon={<FaEyeSlash />} + iconFalse={<FaEye />} + /> + </div> + )} + <div className={`editableText-background ${type}`} style={backgroundProperties} /> </div> - )} - {password && <div className={`password`}> - <Toggle - toggleType={ToggleType.BUTTON} - type={Type.PRIM} - size={size} - color={color} - toggleStatus={showPassword} - onClick={() => setShowPassword(!showPassword)} - tooltip={`${showPassword ? 'Hide' : 'Show'} Password`} - icon={<FaEyeSlash/>} - iconFalse={<FaEye/>} - /> - </div>} - <div className={`editableText-background ${type}`} style={backgroundProperties}/> - </div> - ) + ); -return ( - formLabel ? - <div className={`form-wrapper ${formLabelPlacement}`}> - <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> - {editableText} - </div> - : - editableText - ) -} + return formLabel ? ( + <div className={`form-wrapper ${formLabelPlacement}`}> + <div className={'formLabel'} style={{ fontSize: getFormLabelSize(size) }}> + {formLabel} + </div> + {editableText} + </div> + ) : ( + editableText + ); +}; diff --git a/packages/components/src/components/Group/Group.tsx b/packages/components/src/components/Group/Group.tsx index 7abe4a1c7..6275a09dd 100644 --- a/packages/components/src/components/Group/Group.tsx +++ b/packages/components/src/components/Group/Group.tsx @@ -1,49 +1,33 @@ -import React from 'react' -import './Group.scss' -import { Colors, IGlobalProps, getFontSize, isDark , getFormLabelSize } from '../../global'; +import React from 'react'; +import './Group.scss'; +import { Colors, IGlobalProps, getFontSize, isDark, getFormLabelSize } from '../../global'; export interface IGroupProps extends IGlobalProps { - children: any - rowGap?: number; - columnGap?: number; - padding?: number | string; + children: any; + rowGap?: number; + columnGap?: number; + padding?: number | string; } export const Group = (props: IGroupProps) => { - const { - children, - width = '100%', - rowGap = 5, - columnGap = 5, - padding = 0, - formLabel, - formLabelPlacement, - size, - style, - color, - fillWidth - } = props + const { children, width = '100%', rowGap = 5, columnGap = 5, padding = 0, formLabel, formLabelPlacement, size, style, fillWidth } = props; - const group: JSX.Element = - ( - <div - className="group-wrapper" - style={{ width, padding: padding, ...style }} - > - <div className={`group-container`} - style={{ rowGap, columnGap }} - >{children}</div> - </div> - ) + const group: JSX.Element = ( + <div className="group-wrapper" style={{ width, padding: padding, ...style }}> + <div className={`group-container`} style={{ rowGap, columnGap }}> + {children} + </div> + </div> + ); - return ( - formLabel ? - <div className={`form-wrapper ${formLabelPlacement}`} - style={{ width: fillWidth ? '100%' : undefined}}> - <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> - {group} - </div> - : - group - ) -} + return formLabel ? ( + <div className={`form-wrapper ${formLabelPlacement}`} style={{ width: fillWidth ? '100%' : undefined }}> + <div className={'formLabel'} style={{ fontSize: getFormLabelSize(size) }}> + {formLabel} + </div> + {group} + </div> + ) : ( + group + ); +}; diff --git a/packages/components/src/components/IconButton/IconButton.scss b/packages/components/src/components/IconButton/IconButton.scss index f899dc50f..f95d94ae4 100644 --- a/packages/components/src/components/IconButton/IconButton.scss +++ b/packages/components/src/components/IconButton/IconButton.scss @@ -45,7 +45,7 @@ &.inactive { .background { - filter: opacity(0) !important; + filter: opacity(0); } } @@ -54,13 +54,16 @@ filter: opacity(0); &.active { - filter: opacity(0.2) !important; + filter: opacity(0.3); } } &:hover { .background { - filter: opacity(0.2); + filter: opacity(0.3); + &.active { + filter: opacity(0); + } } } } @@ -70,7 +73,7 @@ filter: opacity(0); &.active { - filter: opacity(0.2) !important; + filter: opacity(0.2); } } @@ -85,10 +88,17 @@ &:hover { box-shadow: global.$standard-button-shadow; } + .background { + filter: opacity(0); + + &.active { + filter: opacity(0.5); + } + } &:hover { .background { - filter: brightness(0.8); + filter: opacity(0.3); } } } diff --git a/packages/components/src/components/IconButton/IconButton.tsx b/packages/components/src/components/IconButton/IconButton.tsx index 048048b20..f61f589b9 100644 --- a/packages/components/src/components/IconButton/IconButton.tsx +++ b/packages/components/src/components/IconButton/IconButton.tsx @@ -1,6 +1,6 @@ import { Tooltip } from '@mui/material'; import React from 'react'; -import { Colors, Size, Type, getFontSize, getHeight, isDark, getFormLabelSize } from '../../global'; +import { Colors, Size, Type, getFontSize, getHeight, getFormLabelSize } from '../../global'; import { IButtonProps } from '../Button'; import './IconButton.scss'; @@ -15,6 +15,7 @@ export const IconButton = (props: IButtonProps) => { type = Type.PRIM, color = Colors.MEDIUM_BLUE, background, + filter, label, height, size = Size.SMALL, @@ -71,6 +72,7 @@ export const IconButton = (props: IButtonProps) => { case Type.SEC: return color; case Type.TERT: + return ''; if (colorPicker) return colorPicker; if (active) return color; else return color; @@ -85,12 +87,7 @@ export const IconButton = (props: IButtonProps) => { case Type.SEC: return color; case Type.TERT: - if (colorPicker) { - if (isDark(colorPicker)) return Colors.WHITE; - else return Colors.BLACK; - } - if (isDark(color)) return Colors.WHITE; - else return Colors.BLACK; + return ''; } }; @@ -102,15 +99,14 @@ export const IconButton = (props: IButtonProps) => { case Type.SEC: return color; case Type.TERT: - if (colorPicker) return colorPicker; - else return color; + return ''; //color; } }; const defaultProperties: React.CSSProperties = { - height: getHeight(+(height ?? 0), size), - width: fillWidth ? '100%' : getHeight(+(height ?? 0), size), - minWidth: getHeight(+(height ?? 0), size), + height: getHeight(height, size), + width: fillWidth ? '100%' : getHeight(height, size), + minWidth: getHeight(height, size), fontWeight: 500, fontSize: getFontSize(size, true), borderColor: getBorderColor(), @@ -119,6 +115,7 @@ export const IconButton = (props: IButtonProps) => { const backgroundProperties: React.CSSProperties = { background: getBackground(), + filter, }; const iconButton: JSX.Element = ( @@ -126,9 +123,9 @@ export const IconButton = (props: IButtonProps) => { <div className={`iconButton-container ${type} ${inactive && 'inactive'}`} onClick={handleClick} onDoubleClick={handleDoubleClick} onPointerDown={handlePointerDown} style={{ ...defaultProperties, ...style }} tabIndex={-1}> <div className="iconButton-content"> {icon} - {colorPicker && type !== Type.TERT && <div className={`color`} style={{ background: colorPicker, outlineColor: defaultProperties.color }} />} + {colorPicker && <div className="color" style={{ background: colorPicker, outlineColor: defaultProperties.color }} />} {label && !hideLabel && ( - <div className={'iconButton-label'} style={{ color: defaultProperties.color }}> + <div className="iconButton-label" style={{ color: defaultProperties.color }}> {label} </div> )} @@ -140,7 +137,7 @@ export const IconButton = (props: IButtonProps) => { return formLabel ? ( <div className={`form-wrapper ${formLabelPlacement}`} style={{ width: fillWidth ? '100%' : undefined }}> - <div className={'formLabel'} style={{ fontSize: getFormLabelSize(size) }}> + <div className="formLabel" style={{ fontSize: getFormLabelSize(size) }}> {formLabel} </div> {iconButton} diff --git a/packages/components/src/components/ListItem/ListItem.tsx b/packages/components/src/components/ListItem/ListItem.tsx index e04c6fbee..6bb3e3824 100644 --- a/packages/components/src/components/ListItem/ListItem.tsx +++ b/packages/components/src/components/ListItem/ListItem.tsx @@ -85,7 +85,7 @@ export const ListItem = (props: IListItemProps) => { <div className="listItem-background" style={{ - background: background ? background : style?.color ? style.color : color, + background: background ?? style?.color ?? color, filter: selected ? 'opacity(0.3)' : isHovered && !inactive ? 'opacity(0.2)' : 'opacity(0)', }} /> diff --git a/packages/components/src/components/MultiToggle/MultiToggle.stories.tsx b/packages/components/src/components/MultiToggle/MultiToggle.stories.tsx index e71423d7a..b9ba45f72 100644 --- a/packages/components/src/components/MultiToggle/MultiToggle.stories.tsx +++ b/packages/components/src/components/MultiToggle/MultiToggle.stories.tsx @@ -1,69 +1,68 @@ -import { Meta, Story } from '@storybook/react' -import React from 'react' -import { IMultiToggleProps, MultiToggle } from './MultiToggle' -import { FaAlignLeft, FaAlignCenter, FaAlignJustify, FaAlignRight } from 'react-icons/fa' +import { Meta, Story } from '@storybook/react'; +import React from 'react'; +import { IMultiToggleProps, MultiToggle } from './MultiToggle'; +import { FaAlignLeft, FaAlignCenter, FaAlignJustify, FaAlignRight } from 'react-icons/fa'; export default { - title: 'Dash/MultiToggle', - component: MultiToggle, - argTypes: {}, -} as Meta<typeof MultiToggle> + title: 'Dash/MultiToggle', + component: MultiToggle, + argTypes: {}, +} as Meta<typeof MultiToggle>; -const MultiToggleStory: Story<IMultiToggleProps> = (args) => <MultiToggle {...args} /> -export const MultiToggleOne = MultiToggleStory.bind({}) +const MultiToggleStory: Story<IMultiToggleProps> = args => <MultiToggle {...args} />; +export const MultiToggleOne = MultiToggleStory.bind({}); MultiToggleOne.args = { - tooltip: "Text alignment", - label: "Alignment", - defaultSelectedItems: "center", - toggleStatus: true, - isToggle: false, - items: [ - { - icon: <FaAlignLeft/>, - tooltip: 'Align left', - val: "left" - }, - { - icon: <FaAlignCenter/>, - tooltip: 'Align center', - val: "center" - }, - { - icon: <FaAlignRight/>, - tooltip: 'Align right', - val: "right" - }, - { - icon: <FaAlignJustify/>, - tooltip: 'Justify', - val: "justify" - }, - ] -} + tooltip: 'Text alignment', + label: 'Alignment', + defaultSelectedItems: 'center', + toggleStatus: true, + items: [ + { + icon: <FaAlignLeft />, + tooltip: 'Align left', + val: 'left', + }, + { + icon: <FaAlignCenter />, + tooltip: 'Align center', + val: 'center', + }, + { + icon: <FaAlignRight />, + tooltip: 'Align right', + val: 'right', + }, + { + icon: <FaAlignJustify />, + tooltip: 'Justify', + val: 'justify', + }, + ], +}; -export const MultiToggleTwo = MultiToggleStory.bind({}) +export const MultiToggleTwo = MultiToggleStory.bind({}); MultiToggleTwo.args = { - tooltip: "Text Tags", - label: "Tags", - defaultSelectedItems : ["left"], - background: "green", - color: 'white', - multiSelect: true, - items: [ - { - icon: <FaAlignLeft/>, - tooltip: 'Like', - val: "left" - }, - { - icon: <FaAlignCenter/>, - tooltip: 'Todo', - val: "center" - }, - { - icon: <FaAlignRight/>, - tooltip: 'Idea', - val: "right" - }, - ] -} + tooltip: 'Text Tags', + label: 'Tags', + defaultSelectedItems: ['left'], + background: 'green', + color: 'white', + multiSelect: true, + items: [ + { + icon: <FaAlignLeft />, + tooltip: 'Like', + val: 'left', + }, + { + icon: <FaAlignCenter />, + tooltip: 'Todo', + val: 'center', + }, + { + icon: <FaAlignRight />, + tooltip: 'Idea', + val: 'right', + }, + ], +}; diff --git a/packages/components/src/components/MultiToggle/MultiToggle.tsx b/packages/components/src/components/MultiToggle/MultiToggle.tsx index 7fff12c8e..c1d610c34 100644 --- a/packages/components/src/components/MultiToggle/MultiToggle.tsx +++ b/packages/components/src/components/MultiToggle/MultiToggle.tsx @@ -1,29 +1,29 @@ -import * as React from 'react' -import { useState } from 'react' -import { Colors, IGlobalProps, Type } from '../../global' -import { Group } from '../Group' -import { IconButton } from '../IconButton' -import { Popup } from '../Popup' -import { IToggleProps, Toggle, ToggleType } from '../Toggle' +import * as React from 'react'; +import { useState } from 'react'; +import { Colors, IGlobalProps, Type } from '../../global'; +import { Group } from '../Group'; +import { IconButton } from '../IconButton'; +import { Popup } from '../Popup'; +import { IToggleProps, Toggle, ToggleType } from '../Toggle'; export interface IToggleItemProps extends IToggleProps { - val: string + val: string; } export interface IMultiToggleProps extends IGlobalProps { - items: IToggleItemProps[] + items: IToggleItemProps[]; multiSelect?: boolean; - defaultSelectedItems?: (string|number) | ((string|number)[]), - selectedItems?: (string | number) | ((string|number)[]), - onSelectionChange?: (val: (string|number) | (string|number)[], added: boolean) => unknown, - isToggle?: boolean; + defaultSelectedItems?: (string | number) | (string | number)[]; + selectedItems?: (string | number) | (string | number)[]; + onSelectionChange?: (val: (string | number) | (string | number)[], added: boolean) => unknown; toggleStatus?: boolean; + showUntilToggle?: boolean; // whether popup stays open when background is clicked. muyst click toggle button tp close it. } -function promoteToArrayOrUndefined(d : (string|number)[]|(string|number)|undefined) { - return d instanceof Array || d === undefined ? d: [d]; +function promoteToArrayOrUndefined(d: (string | number)[] | (string | number) | undefined) { + return d instanceof Array || d === undefined ? d : [d]; } -function promoteToArray(d : (string|number)[]|(string|number)|undefined) { +function promoteToArray(d: (string | number)[] | (string | number) | undefined) { return promoteToArrayOrUndefined(d) ?? []; } @@ -32,56 +32,75 @@ export const MultiToggle = (props: IMultiToggleProps) => { const initVal = (!init ? undefined : promoteToArrayOrUndefined(props.defaultSelectedItems)) ?? promoteToArrayOrUndefined(props.selectedItems) ?? []; init = false; - const [selectedItemsLocal, setSelectedItemsLocal] = useState(initVal as (string|number) | ((string|number)[])); - const { items, selectedItems = selectedItemsLocal, tooltip, tooltipPlacement = 'top', onSelectionChange, color, background } = props; + const [selectedItemsLocal, setSelectedItemsLocal] = useState(initVal as (string | number) | (string | number)[]); + const { + items, // + selectedItems = selectedItemsLocal, + tooltip, + toggleStatus, + tooltipPlacement = 'top', + onSelectionChange, + color, + background, + } = props; const itemsMap = new Map(); - items.forEach((item) => itemsMap.set(item.val, item)); - return <div className={`multiToggle-container`}> - <Popup - toggle={props.isToggle? undefined : <div style={{position: "relative"}}> - <IconButton - color={color} - borderColor={background ? color : undefined} - label={props.label} - active={props.toggleStatus} - background={background} - type={color && background ? Type.TERT : undefined} - {...(itemsMap.get(promoteToArray(selectedItems)[0]) ?? {})} - tooltip={tooltip} - tooltipPlacement={tooltipPlacement} - /> - {promoteToArray(selectedItems).length < 2 ? null : - <div style={{position: "absolute", top: "0", left: "0", color: color ?? Colors.MEDIUM_BLUE}}> - + - </div>} - </div>} - isToggle={props.isToggle} - toggleFunc={() => { - const selItem = items.find(item => promoteToArray(selectedItems).includes(item.val)); - selItem && setSelectedItemsLocal([selItem.val]); - }} - type={props.type} - label={props.isToggle ? props.label : undefined} - toggleStatus={props.isToggle ? props.toggleStatus : undefined} - color={color} - popup={<Group padding={5} color={color} columnGap={0} style={{overflow: 'hidden'}}> - {items.map((item, i) => - <Toggle key={i} color={color} icon={item.icon} tooltip={item.tooltip} - toggleStatus={promoteToArray(selectedItems).includes(item.val)} - type={Type.PRIM} - toggleType={ToggleType.BUTTON} - onClick={e => { - const selected = new Set<string|number>(); - promoteToArray(selectedItems).forEach(val => val && selected.add(val)); - const toAdd = !props.multiSelect || !selected.has(item.val) - if (!toAdd) selected.delete(item.val); - else item.val && selected.add(item.val); - onSelectionChange?.(item.val, toAdd); - setSelectedItemsLocal(props.multiSelect ? Array.from(selected) : item.val); - e.stopPropagation(); - }}/> - )} - </Group>} - /> - </div> -}
\ No newline at end of file + items.forEach(item => itemsMap.set(item.val, item)); + return ( + <div className="multiToggle-container"> + <Popup + isOpen={toggleStatus} + multitoggle={true} // this is used to indicate that this is a multi toggle, so it can be styled differently in the popup + toggle={ + <div style={{ position: 'relative' }}> + <IconButton + color={color} + borderColor={background ? color : undefined} + label={props.label} + active={props.toggleStatus} + background={color} + {...(itemsMap.get(promoteToArray(selectedItems)[0]) ?? {})} + tooltip={tooltip} + tooltipPlacement={tooltipPlacement} + /> + {promoteToArray(selectedItems).length < 2 ? null : <div style={{ position: 'absolute', top: '0', left: '0', color: color ?? Colors.MEDIUM_BLUE }}>+</div>} + </div> + } + showUntilToggle={props.showUntilToggle ?? true} + toggleFunc={() => { + const selItem = items.find(item => promoteToArray(selectedItems).includes(item.val)); + selItem && setSelectedItemsLocal([selItem.val]); + }} + type={props.type} + label={undefined} + color={color} + background={background} + popup={ + <Group padding={5} color={color} background={background} columnGap={0} style={{ overflow: 'hidden' }}> + {items.map((item, i) => ( + <Toggle + key={i} + color={color} + background={color} + icon={item.icon} + tooltip={item.tooltip} + toggleStatus={promoteToArray(selectedItems).includes(item.val)} + type={Type.PRIM} + toggleType={ToggleType.BUTTON} + onClick={e => { + const selected = new Set<string | number>(); + promoteToArray(selectedItems).forEach(val => val && selected.add(val)); + const toAdd = !props.multiSelect || !selected.has(item.val); + if (!toAdd) selected.delete(item.val); + else item.val && selected.add(item.val); + onSelectionChange?.(item.val, toAdd); + setSelectedItemsLocal(props.multiSelect ? Array.from(selected) : item.val); + e.stopPropagation(); + }} + /> + ))} + </Group> + } + /> + </div> + ); +}; diff --git a/packages/components/src/components/NumberDropdown/NumberDropdown.tsx b/packages/components/src/components/NumberDropdown/NumberDropdown.tsx index 7f12198d5..b5f55bacf 100644 --- a/packages/components/src/components/NumberDropdown/NumberDropdown.tsx +++ b/packages/components/src/components/NumberDropdown/NumberDropdown.tsx @@ -20,11 +20,41 @@ export interface INumberDropdownProps extends INumberProps { export const NumberDropdown = (props: INumberDropdownProps) => { const [numberLoc, setNumberLoc] = useState<number>(0); - const { fillWidth, numberDropdownType = false, color = Colors.MEDIUM_BLUE, type, formLabelPlacement, showPlusMinus, min, max, unit, step = 1, number = numberLoc, setNumber = setNumberLoc, size, formLabel, tooltip } = props; + const { + fillWidth, // + numberDropdownType = false, + color = Colors.MEDIUM_BLUE, + type, + formLabelPlacement, + showPlusMinus, + min, + max, + unit, + background, + step = 1, + number = numberLoc, + setNumber = setNumberLoc, + size, + formLabel, + tooltip, + } = props; const [isOpen, setOpen] = useState<boolean>(false); let toggleText = number.toString(); if (unit) toggleText = toggleText + unit; - let toggle = <Toggle tooltip={tooltip} color={color} fillWidth={fillWidth} type={type} size={size} align={'center'} text={toggleText} toggleType={ToggleType.BUTTON} toggleStatus={isOpen} onPointerDown={() => setOpen(!isOpen)} />; + let toggle = ( + <Toggle + tooltip={tooltip} // + color={color} + fillWidth={fillWidth} + type={type} + size={size} + align="center" + text={toggleText} + toggleType={ToggleType.BUTTON} + toggleStatus={isOpen} + onPointerDown={() => setOpen(!isOpen)} + /> + ); if (showPlusMinus) { toggle = ( @@ -75,23 +105,23 @@ export const NumberDropdown = (props: INumberDropdownProps) => { break; case 'slider': default: - popup = <Slider size={Size.SMALL} unit={unit} multithumb={false} min={min} max={max} step={step} number={number} setNumber={setNumber} />; + popup = <Slider size={Size.SMALL} unit={unit} background={background} multithumb={false} min={min} max={max} step={step} number={number} setNumber={setNumber} />; break; case 'input': - popup = <Slider multithumb={false} min={min} max={max} step={step} number={number} />; + popup = <Slider multithumb={false} min={min} background={background} max={max} step={step} number={number} />; break; } const numberDropdown: JSX.Element = ( - <div className={`numberDropdown-container`} style={{ height: '75%', width: fillWidth ? '100%' : 'fit-content' }}> - <Popup setOpen={setOpen} placement={'bottom'} size={size} isOpen={isOpen} popup={popup} toggle={toggle} fillWidth={fillWidth} color={color} /> + <div className="numberDropdown-container" style={{ height: '75%', width: fillWidth ? '100%' : 'fit-content' }}> + <Popup setOpen={setOpen} placement="bottom" size={size} isOpen={isOpen} popup={popup} toggle={toggle} fillWidth={fillWidth} color={color} background={background} /> </div> ); return formLabel ? ( - <div className={`form-wrapper ${formLabelPlacement}`} style={{ height: '100%', width: fillWidth ? '100%' : undefined }}> + <div className={`form-wrapper ${formLabelPlacement}`} style={{ color, height: '100%', width: fillWidth ? '100%' : undefined }}> {numberDropdown} - <div className="iconButton-label" style={{ fontSize: getFormLabelSize(size) }}> + <div className="iconButton-label" onPointerDown={() => setOpen(!isOpen)} style={{ cursor: 'pointer', height: '25%', fontSize: getFormLabelSize(size) }}> {formLabel} </div> </div> diff --git a/packages/components/src/components/NumberInput/NumberInput.tsx b/packages/components/src/components/NumberInput/NumberInput.tsx index 33573d000..56c61a41e 100644 --- a/packages/components/src/components/NumberInput/NumberInput.tsx +++ b/packages/components/src/components/NumberInput/NumberInput.tsx @@ -1,89 +1,76 @@ -import * as React from 'react' -import { Colors, INumberProps , Type, getFormLabelSize, getHeight } from '../../global' -import './NumberInput.scss' -import { useState } from 'react' -import { Group } from '../Group' -import { Toggle, ToggleType } from '../Toggle' -import { IconButton } from '../IconButton' -import * as fa from 'react-icons/fa' -import { EditableText } from '../EditableText' +import * as React from 'react'; +import { Colors, INumberProps, getFormLabelSize, getHeight } from '../../global'; +import './NumberInput.scss'; +import { useState } from 'react'; +import { Group } from '../Group'; +import { IconButton } from '../IconButton'; +import * as fa from 'react-icons/fa'; +import { EditableText } from '../EditableText'; export interface INumberInputProps extends INumberProps { - showPlusMinus?: boolean + showPlusMinus?: boolean; } export const NumberInput = (props: INumberInputProps) => { - const [numberLoc, setNumberLoc] = useState<number>(10) - const { - color = Colors.MEDIUM_BLUE, - type, - formLabelPlacement, - showPlusMinus, - min, - max, - unit = '', - width, - fillWidth = width ? true : false, - step = 1, - number = numberLoc, - setNumber = setNumberLoc, - size, - formLabel, - tooltip } = - props; + const [numberLoc, setNumberLoc] = useState<number>(10); + const { color = Colors.MEDIUM_BLUE, type, formLabelPlacement, showPlusMinus, min, max, unit = '', width, fillWidth = width ? true : false, step = 1, number = numberLoc, setNumber = setNumberLoc, size, formLabel } = props; + + let input = ( + <EditableText + color={color} + type={type} + size={size} + val={number.toString() + unit} + // width={getHeight(undefined, size)} + textAlign={'center'} + fillWidth={fillWidth} + width={width && width - (showPlusMinus ? +getHeight(undefined, size) * 4 : 0)} + setVal={val => setNumber(!isNaN(Number(val)) ? Number(val) : number)} + /> + ); - let input = <EditableText - color={color} - type={type} - size={size} - val={number.toString() + unit} - // width={getHeight(undefined, size)} - textAlign={'center'} - fillWidth={fillWidth} - width={width && width - (showPlusMinus ? getHeight(undefined, size) * 4 : 0)} - setVal={val => setNumber(!isNaN(Number(val)) ? Number(val) : number)} - />; - if (showPlusMinus) { - input = <Group columnGap={0} style={{overflow: 'hidden'}}> - {input} - <IconButton - size={size} - icon={<fa.FaMinus/>} - color={color} - onClick={(e) => { - e.stopPropagation(); - setNumber(number - step); - }} - inactive={number - step < min} - tooltip={`Subtract ${step}${unit}`} - /> - <IconButton - size={size} - icon={<fa.FaPlus/>} - color={color} - onClick={(e) => { - e.stopPropagation(); - setNumber(number + step); - }} - inactive={number + step > max} - tooltip={`Add ${step}${unit}`} - /> - </Group> + input = ( + <Group columnGap={0} style={{ overflow: 'hidden' }}> + {input} + <IconButton + size={size} + icon={<fa.FaMinus />} + color={color} + onClick={e => { + e.stopPropagation(); + setNumber(number - step); + }} + inactive={number - step < min} + tooltip={`Subtract ${step}${unit}`} + /> + <IconButton + size={size} + icon={<fa.FaPlus />} + color={color} + onClick={e => { + e.stopPropagation(); + setNumber(number + step); + }} + inactive={number + step > max} + tooltip={`Add ${step}${unit}`} + /> + </Group> + ); } - - return ( - formLabel ? - <div className={`form-wrapper ${formLabelPlacement}`} style={{ width: fillWidth ? '100%' : undefined}}> - <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> - <div className={`numberInput-container`} style={{width: fillWidth ? '100%' : 'fit-content'}}> + return formLabel ? ( + <div className={`form-wrapper ${formLabelPlacement}`} style={{ width: fillWidth ? '100%' : undefined }}> + <div className={'formLabel'} style={{ fontSize: getFormLabelSize(size) }}> + {formLabel} + </div> + <div className={`numberInput-container`} style={{ width: fillWidth ? '100%' : 'fit-content' }}> {input} </div> </div> - : - <div className={`numberInput-container`} style={{width: fillWidth ? '100%' : 'fit-content'}}> + ) : ( + <div className={`numberInput-container`} style={{ width: fillWidth ? '100%' : 'fit-content' }}> {input} </div> - ) -}
\ No newline at end of file + ); +}; diff --git a/packages/components/src/components/Popup/Popup.tsx b/packages/components/src/components/Popup/Popup.tsx index 5a58fee29..9e91a75cf 100644 --- a/packages/components/src/components/Popup/Popup.tsx +++ b/packages/components/src/components/Popup/Popup.tsx @@ -1,8 +1,9 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Colors, IGlobalProps, Placement, Size, isDark } from '../../global'; +import { IGlobalProps, Placement, Size } from '../../global'; import { Toggle, ToggleType } from '../Toggle'; import './Popup.scss'; import { Popper } from '@mui/material'; +import PositionObserver from '@thednp/position-observer'; export enum PopupTrigger { CLICK = 'click', @@ -20,13 +21,13 @@ export interface IPopupProps extends IGlobalProps { toggle?: JSX.Element; popup: JSX.Element | string | (() => JSX.Element); trigger?: PopupTrigger; - toggleStatus?: boolean; isOpen?: boolean; setOpen?: (b: boolean) => void; background?: string; - isToggle?: boolean; + showUntilToggle?: boolean; // whether popup stays open when background is clicked. muyst click toggle button tp close it. toggleFunc?: () => void; popupContainsPt?: (x: number, y: number) => boolean; + multitoggle?: boolean; } /** @@ -57,20 +58,22 @@ export const Popup = (props: IPopupProps) => { height, fillWidth, iconPlacement = 'left', - background = isDark(color) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY, + background, + multitoggle, popupContainsPt, } = props; const triggerRef = useRef(null); const popperRef = useRef<HTMLDivElement | null>(null); - const toggleRef = useRef<HTMLDivElement | null>(null); + const [toggleRef, setToggleRef] = useState<HTMLDivElement | null>(null); let timeout = setTimeout(() => {}); const handlePointerAwayDown = (e: PointerEvent) => { const rect = popperRef.current?.getBoundingClientRect(); - const rect2 = toggleRef.current?.getBoundingClientRect(); + const rect2 = toggleRef?.getBoundingClientRect(); if ( + !props.showUntilToggle && (!rect2 || !(rect2.left < e.clientX && rect2.top < e.clientY && rect2.right > e.clientX && rect2.bottom > e.clientY)) && rect && !(rect.left < e.clientX && rect.top < e.clientY && rect.right > e.clientX && rect.bottom > e.clientY) && @@ -82,22 +85,36 @@ export const Popup = (props: IPopupProps) => { } }; + let observer: PositionObserver | undefined = undefined; + const [previousPosition, setPreviousPosition] = useState<DOMRect | undefined>(toggleRef?.getBoundingClientRect()); + useEffect(() => { if (isOpen) { window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true }); window.addEventListener('pointerdown', handlePointerAwayDown, { capture: true }); - return () => window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true }); - } - }, [isOpen, popupContainsPt]); - + if (toggleRef && multitoggle) { + (observer = new PositionObserver(entries => { + entries.forEach(entry => { + const currentPosition = entry.boundingClientRect; + if (Math.floor(currentPosition.top) !== Math.floor(previousPosition?.top ?? 0) || Math.floor(currentPosition.left) !== Math.floor(previousPosition?.left ?? 0)) { + // Perform actions when position changes + setPreviousPosition(currentPosition); // Update previous position + } + }); + })).observe(toggleRef); + } + return () => { + window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true }); + observer?.disconnect(); + }; + } else observer?.disconnect(); + }, [isOpen, toggleRef, popupContainsPt]); return ( <div className={`popup-wrapper ${fillWidth && 'fillWidth'}`}> <div - className={`trigger-container ${fillWidth && 'fillWidth'}`} ref={triggerRef} - onClick={() => { - if (trigger === PopupTrigger.CLICK) setOpen(!isOpen); - }} + className={`trigger-container ${fillWidth && 'fillWidth'}`} + onClick={() => trigger === PopupTrigger.CLICK && setOpen(!isOpen)} onPointerEnter={() => { if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { clearTimeout(timeout); @@ -109,42 +126,38 @@ export const Popup = (props: IPopupProps) => { timeout = setTimeout(() => setOpen(false), 1000); } }}> - {toggle ?? ( - <div ref={toggleRef}> + <div className="special" ref={R => setToggleRef(R)}> + {toggle ?? ( <Toggle tooltip={tooltip} size={size} type={type} color={color} - background={props.isToggle ? undefined : background} + background={background} toggleType={ToggleType.BUTTON} icon={icon} iconPlacement={iconPlacement} text={text} label={props.label} - toggleStatus={isOpen || props.toggleStatus} + toggleStatus={isOpen} onClick={() => { if (trigger === PopupTrigger.CLICK) { - if (!props.isToggle || props.toggleStatus) { - setOpen(!isOpen); - } + setOpen(!isOpen); props.toggleFunc?.(); } }} fillWidth={fillWidth} /> - </div> - )} + )} + </div> </div> <Popper open={isOpen} style={{ zIndex: 20000 }} anchorEl={triggerRef.current} placement={placement} modifiers={[]}> <div - className={`popup-container`} + className="popup-container" ref={popperRef} style={{ width, height, background }} tabIndex={-1} - onPointerDown={e => { - e.stopPropagation(); - }} + onPointerDown={e => e.stopPropagation()} onPointerEnter={() => { if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { clearTimeout(timeout); diff --git a/packages/components/src/components/Slider/Slider.tsx b/packages/components/src/components/Slider/Slider.tsx index 5af945383..fefc14832 100644 --- a/packages/components/src/components/Slider/Slider.tsx +++ b/packages/components/src/components/Slider/Slider.tsx @@ -52,7 +52,7 @@ export const Slider = (props: ISliderProps) => { const toDecimal = (num: number) => (decimals !== undefined ? Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals) : num); const getLeftPos = (locVal: number) => { - const dragger = getHeight(+(height || 0), size); + const dragger = +getHeight(height, size); return ((locVal - min) / (max - min)) * (width - dragger); }; @@ -65,8 +65,8 @@ export const Slider = (props: ISliderProps) => { background: color, color: isDark(color) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY, fontSize: getFontSize(size), - height: getHeight(+(height || 0), size), - width: getHeight(+(height || 0), size), + height: getHeight(height, size), + width: getHeight(height, size), top: 0, }}> <span className="rs-label">{toDecimal(locVal)}</span> @@ -132,7 +132,7 @@ export const Slider = (props: ISliderProps) => { lastEndVal = endNumberLoc; }} style={{ - padding: `5px 0px ${getHeight(+(height || 0), size)}px 0px`, + padding: `5px 0px ${getHeight(height, size)}px 0px`, width: fillWidth ? '100%' : 'fit-content', }}> <div @@ -141,22 +141,22 @@ export const Slider = (props: ISliderProps) => { r && new ResizeObserver(() => setWidth(+(r?.clientWidth ?? 100))).observe(r); setWidth(+(r?.clientWidth ?? 100)); }} - style={{ height: getHeight(+(height || 0), size) }} + style={{ height: getHeight(height, size) }} onPointerDown={onPointerDown}> {ValSlider} <div className="selected-range" style={{ - height: getHeight(+(height || 0), size) / 10, + height: +getHeight(height, size) / 10, background: multithumb ? Colors.LIGHT_GRAY : color, }} /> <div className="range" style={{ - height: getHeight(+(height || 0), size) / 10, + height: +getHeight(height, size) / 10, width: getLeftPos(endNumber) - getLeftPos(number), - left: getLeftPos(number) + getHeight(+(height || 0), size), + left: getLeftPos(number) + +getHeight(height, size), display: multithumb ? undefined : 'none', background: color, }} diff --git a/packages/components/src/components/Toggle/Toggle.tsx b/packages/components/src/components/Toggle/Toggle.tsx index 5cc2ae339..371d00cc1 100644 --- a/packages/components/src/components/Toggle/Toggle.tsx +++ b/packages/components/src/components/Toggle/Toggle.tsx @@ -1,169 +1,170 @@ -import { Tooltip } from '@mui/material' -import React, { useState } from 'react' -import * as bi from 'react-icons/bi' -import { IGlobalProps, Placement, Type , getFormLabelSize } from '../../global' -import { Size } from '../../global/globalEnums' -import { getFontSize, getHeight } from '../../global/globalUtils' -import { Button, IButtonProps } from '../Button' -import { IconButton } from '../IconButton' -import './Toggle.scss' +import { Tooltip } from '@mui/material'; +import React, { useState } from 'react'; +import * as bi from 'react-icons/bi'; +import { Type, getFormLabelSize } from '../../global'; +import { Size } from '../../global/globalEnums'; +import { getFontSize, getHeight } from '../../global/globalUtils'; +import { Button, IButtonProps } from '../Button'; +import { IconButton } from '../IconButton'; +import './Toggle.scss'; export enum ToggleType { - BUTTON = "button", - CHECKBOX = "checkbox", - SWITCH = "switch", + BUTTON = 'button', + CHECKBOX = 'checkbox', + SWITCH = 'switch', } export interface IToggleProps extends IButtonProps { - toggleStatus?: boolean // true -> selected, false -> unselected - toggleType?: ToggleType - iconFalse?: JSX.Element | string + toggleStatus?: boolean; // true -> selected, false -> unselected + toggleType?: ToggleType; + iconFalse?: JSX.Element | string; + triState?: boolean; } export const Toggle = (props: IToggleProps) => { - const [toggleStatusLoc, setToggleStatusLoc] = useState<boolean>(true); - const { - toggleStatus = toggleStatusLoc, - toggleType = ToggleType.CHECKBOX, - type = Type.SEC, - style, - color, - background, - text, - icon, - iconFalse = icon, - height, - inactive, - label, - iconPlacement, - onPointerDown, - onClick, - tooltip, - tooltipPlacement = 'top', - size = Size.SMALL, - formLabel, - formLabelPlacement, - fillWidth, - align - } = props + const [toggleStatusLoc, setToggleStatusLoc] = useState<boolean>(true); + const { + toggleStatus = toggleStatusLoc, + toggleType = ToggleType.CHECKBOX, + type = Type.SEC, + color, + background, + text, + icon, + iconFalse = icon, + height, + inactive, + label, + iconPlacement, + onPointerDown, + onClick, + triState, + tooltip, + tooltipPlacement = 'top', + size = Size.SMALL, + formLabel, + formLabelPlacement, + fillWidth, + align, + } = props; - /** - * Pointer down - * @param e - */ - const handlePointerDown = (e: React.PointerEvent) => { - if (!inactive && onPointerDown){ - e.stopPropagation(); - e.preventDefault(); - onPointerDown(e) - } - } + /** + * Pointer down + * @param e + */ + const handlePointerDown = (e: React.PointerEvent) => { + if (!inactive && onPointerDown) { + e.stopPropagation(); + e.preventDefault(); + onPointerDown(e); + } + }; - /** - * Single click - * @param e - */ - const handleClick = (e: React.MouseEvent) => { - if (toggleStatus === toggleStatusLoc) { - setToggleStatusLoc(!toggleStatus) - } - - if (!inactive && onClick) { - e.stopPropagation(); - e.preventDefault(); - onClick(e); - } - } + /** + * Single click + * @param e + */ + const handleClick = (e: React.MouseEvent) => { + if (toggleStatus === toggleStatusLoc) { + setToggleStatusLoc(!toggleStatus); + } - const defaultProperties = { - height: getHeight(height, size), - borderColor: color - } + if (!inactive && onClick) { + e.stopPropagation(); + e.preventDefault(); + onClick(e); + } + }; - let toggleElement: JSX.Element; + const defaultProperties = { + height: getHeight(height, size), + borderColor: color, + }; - switch(toggleType) { - case ToggleType.BUTTON: - toggleElement = ( - <Button - text={text} - tooltip={tooltip} - icon={toggleStatus ? icon : iconFalse} - onPointerDown={handlePointerDown} - onClick={handleClick} - active={toggleStatus} - type={type} - size={size} - iconPlacement={iconPlacement} - color={color} - background={background} - label={label} - fillWidth={fillWidth} - align={align} - /> - ); - break; - case ToggleType.CHECKBOX: - toggleElement = ( - <IconButton - icon={ - toggleStatus ? <bi.BiCheck/> : undefined - } - tooltip={tooltip} - onPointerDown={handlePointerDown} - onClick={handleClick} - active={toggleStatus} - type={type} - size={size} - color={color} - background={background} - label={label} - fillWidth={fillWidth} - align={align} - /> - ); - break; - case ToggleType.SWITCH: - default: - toggleElement = ( - <Tooltip disableInteractive={true} arrow={true} placement={tooltipPlacement} title={tooltip}> - <div - className={`toggle-container ${toggleType}`} - onPointerDown={handlePointerDown} - onClick={handleClick} - style={{ - width: 2*getHeight(height, size), - ...defaultProperties - }} - > - <div className="toggle-content" style={{ - fontSize: getFontSize(size), - borderColor: color, - left: toggleStatus ? '0%' : `calc(100% - ${getHeight(height, size)}px)` - }}> - <div className="toggle-switch" style={{ - width: getHeight(height, size), - height: getHeight(height, size), - background: color - }}></div> - </div> - <div className={`toggle-background ${toggleStatus && 'active'}`} - style={{ background: color}} - /> - </div> - </Tooltip> - ); - break; - } + let toggleElement: JSX.Element; + switch (toggleType) { + case ToggleType.BUTTON: + toggleElement = ( + <Button + text={text} + tooltip={tooltip} + icon={toggleStatus ? icon : iconFalse} + onPointerDown={handlePointerDown} + onClick={handleClick} + active={toggleStatus} + type={type} + size={size} + iconPlacement={iconPlacement} + color={color} + background={background} + label={label} + fillWidth={fillWidth} + align={align} + filter={triState ? 'opacity(0.2)' : undefined} + /> + ); + break; + case ToggleType.CHECKBOX: + toggleElement = ( + <IconButton + icon={toggleStatus ? <bi.BiCheck /> : undefined} + tooltip={tooltip} + onPointerDown={handlePointerDown} + onClick={handleClick} + active={toggleStatus} + type={type} + size={size} + color={color} + background={background} + label={label} + fillWidth={fillWidth} + align={align} + /> + ); + break; + case ToggleType.SWITCH: + default: + toggleElement = ( + <Tooltip disableInteractive={true} arrow={true} placement={tooltipPlacement} title={tooltip}> + <div + className={`toggle-container ${toggleType}`} + onPointerDown={handlePointerDown} + onClick={handleClick} + style={{ + width: 2 * +getHeight(height, size), + ...defaultProperties, + }}> + <div + className="toggle-content" + style={{ + fontSize: getFontSize(size), + borderColor: color, + left: toggleStatus ? '0%' : `calc(100% - ${getHeight(height, size)}px)`, + }}> + <div + className="toggle-switch" + style={{ + width: getHeight(height, size), + height: getHeight(height, size), + background: color, + }}></div> + </div> + <div className={`toggle-background ${toggleStatus && 'active'}`} style={{ background: color }} /> + </div> + </Tooltip> + ); + break; + } - return ( - formLabel ? - <div className={`form-wrapper ${formLabelPlacement}`}> - <div className={'formLabel'} style={{fontSize: getFormLabelSize(size)}}>{formLabel}</div> - {toggleElement} - </div> - : - toggleElement - ) -} + return formLabel ? ( + <div className={`form-wrapper ${formLabelPlacement}`}> + <div className={'formLabel'} style={{ fontSize: getFormLabelSize(size) }}> + {formLabel} + </div> + {toggleElement} + </div> + ) : ( + toggleElement + ); +}; diff --git a/packages/components/src/global/globalEnums.tsx b/packages/components/src/global/globalEnums.tsx index bdeacccdb..bd82e3c57 100644 --- a/packages/components/src/global/globalEnums.tsx +++ b/packages/components/src/global/globalEnums.tsx @@ -1,52 +1,53 @@ export enum Colors { - BLACK = "#000000", - DARK_GRAY = "#323232", - MEDIUM_GRAY = "#9F9F9F", - LIGHT_GRAY = "#DFDFDF", - WHITE = "#FFFFFF", - MEDIUM_BLUE = "#4476F7", - MEDIUM_BLUE_ALT = "#4476f73d", // REDUCED OPACITY - LIGHT_BLUE = "#BDDDF5", - PINK = "#E0217D", - YELLOW = "#F5D747", - DROP_SHADOW = "#32323215", - ERROR_RED = "#FF9494", - SUCCESS_GREEN = "#4BB543", - TRANSPARENT = "transparent" + BLACK = '#000000', + DARK_GRAY = '#323232', + MEDIUM_GRAY = '#9F9F9F', + LIGHT_GRAY = '#DFDFDF', + WHITE = '#FFFFFF', + MEDIUM_BLUE = '#4476F7', + MEDIUM_BLUE_ALT = '#4476f73d', // REDUCED OPACITY + LIGHT_BLUE = '#BDDDF5', + PINK = '#E0217D', + YELLOW = '#F5D747', + DROP_SHADOW = '#32323215', + ERROR_RED = '#FF9494', + SUCCESS_GREEN = '#4BB543', + TRANSPARENT = 'transparent', } export enum FontSize { - JUMBO_ICON = "5rem", - ICON = "3rem", - HEADER = "1.6rem", - DEFAULT = "1rem", - SECONDARY = "1.3rem", - LABEL = "0.6rem" + JUMBO_ICON = '5rem', + ICON = '3rem', + HEADER = '1.6rem', + DEFAULT = '1rem', + SECONDARY = '1.3rem', + LABEL = '0.6rem', } export enum Padding { - MINIMUM_PADDING = "4px", - SMALL_PADDING = "8px", - MEDIUM_PADDING = "16px", - LARGE_PADDING = "32px", + MINIMUM_PADDING = '4px', + SMALL_PADDING = '8px', + MEDIUM_PADDING = '16px', + LARGE_PADDING = '32px', } export enum IconSizes { - ICON_SIZE = "28px", + ICON_SIZE = '28px', } export enum Borders { - STANDARD = "solid 1px #9F9F9F", - STANDARD_BORDER_RADIUS = '5px' + STANDARD = 'solid 1px #9F9F9F', + STANDARD_BORDER_RADIUS = '5px', } export enum Shadows { - STANDARD_SHADOW = "0px 3px 4px rgba(0, 0, 0, 0.3)" + STANDARD_SHADOW = '0px 3px 4px rgba(0, 0, 0, 0.3)', } export enum Size { - XSMALL = "xsmall", - SMALL = "small", - MEDIUM = "medium", - LARGE = "large" -}
\ No newline at end of file + XXSMALL = 'xxsmall', + XSMALL = 'xsmall', + SMALL = 'small', + MEDIUM = 'medium', + LARGE = 'large', +} diff --git a/packages/components/src/global/globalUtils.tsx b/packages/components/src/global/globalUtils.tsx index 0f1e0adbc..4a1c2cb66 100644 --- a/packages/components/src/global/globalUtils.tsx +++ b/packages/components/src/global/globalUtils.tsx @@ -1,93 +1,88 @@ -import { Size } from './globalEnums' +import { Size } from './globalEnums'; import Color from 'color'; export interface ILocation { - top: number - left: number - width: number - height: number - override?: 'left' | 'bottom' | 'top' | 'right' + top: number; + left: number; + width: number; + height: number; + override?: 'left' | 'bottom' | 'top' | 'right'; } -export const getFormLabelSize = ( - size: Size | undefined, -) => { - switch (size) { - case Size.XSMALL: - return '7px' - case Size.SMALL: - return '10px' - case Size.MEDIUM: - return '13px' - case Size.LARGE: - return '14px' - default: - return '10px' - } -} +export const getFormLabelSize = (size: Size | undefined) => { + switch (size) { + case Size.XXSMALL: + return '6px'; + case Size.XSMALL: + return '7px'; + case Size.SMALL: + return '10px'; + case Size.MEDIUM: + return '13px'; + case Size.LARGE: + return '14px'; + default: + return '10px'; + } +}; -export const getFontSize = ( - size: Size | undefined, - icon?: boolean -) => { - switch (size) { - case Size.XSMALL: - if (icon) return '11px' - return '9px' - case Size.SMALL: - if (icon) return '15px' - return '11px' - case Size.MEDIUM: - if (icon) return '17px' - return '14px' - case Size.LARGE: - if (icon) return '22px' - return '17px' - default: - if (icon) return '15px' - return '12px' - } -} +export const getFontSize = (size: Size | undefined, icon?: boolean) => { + switch (size) { + case Size.XXSMALL: + if (icon) return '10px'; + return '7px'; + case Size.XSMALL: + if (icon) return '11px'; + return '9px'; + case Size.SMALL: + if (icon) return '15px'; + return '11px'; + case Size.MEDIUM: + if (icon) return '17px'; + return '14px'; + case Size.LARGE: + if (icon) return '22px'; + return '17px'; + default: + if (icon) return '15px'; + return '12px'; + } +}; -export const getHeight = ( - height: number | undefined, - size: Size | undefined -) => { - if (height) return height - switch (size) { - case Size.XSMALL: - return 20 - case Size.SMALL: - return 30 - case Size.MEDIUM: - return 40 - case Size.LARGE: - return 50 - default: - return 30 - } -} +export const getHeight = (height: number | string | undefined, size: Size | undefined) => { + if (height) return height; + switch (size) { + case Size.XXSMALL: + return 15; + case Size.XSMALL: + return 20; + case Size.SMALL: + return 30; + case Size.MEDIUM: + return 40; + case Size.LARGE: + return 50; + default: + return 30; + } +}; -export const colorConvert = (color: any) => { - try { - return color ? Color(color.toLowerCase()) : Color('transparent') - } catch (e) { - console.log('COLOR error:', e) - return Color('red') - } -} +export const colorConvert = (color: string) => { + try { + return color ? Color(color.toLowerCase()) : Color('transparent'); + } catch (e) { + console.log('COLOR error:', e); + return Color('red'); + } +}; -export const isDark = (color: any): boolean => { - if (color === undefined) return false - if (color === 'transparent') return false - if (color.startsWith?.('linear')) return false - const nonAlphaColor = color.startsWith('#') - ? (color as string).substring(0, 7) - : color.startsWith('rgba') - ? color.replace(/,.[^,]*\)/, ')').replace('rgba', 'rgb') - : color - const col = colorConvert(nonAlphaColor).rgb() - const colsum = col.red() + col.green() + col.blue() - if (colsum / col.alpha() > 400 || col.alpha() < 0.25) return false - else return true -} +export const isDark = (color: string): boolean => { + if (color === undefined) return false; + if (color === 'transparent') return false; + if (color.startsWith?.('linear')) return false; + const nonAlphaColor = color.startsWith('#') ? (color as string).substring(0, 7) : color.startsWith('rgba') ? color.replace(/,.[^,]*\)/, ')').replace('rgba', 'rgb') : color; + const col = colorConvert(nonAlphaColor).rgb(); + const colsum = col.red() + col.green() + col.blue(); + if (colsum / col.alpha() > 400 || col.alpha() < 0.25) return false; + else return true; +}; diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 29b6ab989..9cb47995c 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -1,5 +1,6 @@ import { ChatCompletionMessageParam, Image } from 'openai/resources'; import { openai } from './setup'; +import { imageUrlToBase64 } from '../../../ClientUtils'; export enum GPTDocCommand { AssignTags = 1, @@ -270,7 +271,6 @@ const gptImageLabel = async (src: string, prompt: string): Promise<string> => { ], }); if (response.choices[0].message.content) { - console.log(response.choices[0].message.content); return response.choices[0].message.content; } return 'Missing labels'; @@ -310,7 +310,9 @@ const gptHandwriting = async (src: string): Promise<string> => { } }; -const gptDescribeImage = async (image: string): Promise<string> => { +const gptDescribeImage = async (userPrompt: string, url: string): Promise<string> => { + if (userPrompt) return userPrompt; + const image = imageUrlToBase64(url); try { const response = await openai.chat.completions.create({ model: 'gpt-4o', diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 0bb6c0ce9..f5eee84aa 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -283,10 +283,10 @@ export namespace DocUtils { } return val1 === val2; }; - Object.entries(reqdOpts).forEach(pair => { - const targetDoc = pair[0].startsWith('_') ? doc : Doc.GetProto(doc as Doc); - if (!Object.getOwnPropertyNames(targetDoc).includes(pair[0].replace(/^_/, '')) || !compareValues(pair[1], targetDoc[pair[0]])) { - targetDoc[pair[0]] = pair[1]; + Object.entries(reqdOpts).forEach(([key, val]) => { + const targetDoc = key.startsWith('_') ? doc : Doc.GetProto(doc as Doc); + if (!Object.getOwnPropertyNames(targetDoc).includes(key.replace(/^_/, '')) || !compareValues(val, targetDoc[key])) { + targetDoc[key] = val as FieldType; } }); items?.forEach(item => !DocListCast(doc.data).includes(item) && Doc.AddDocToList(Doc.GetProto(doc), 'data', item)); @@ -578,7 +578,7 @@ export namespace DocUtils { Docs.Create.FreeformDocument([], { _width: NumCast(layoutDoc._width) + 10, _height: Math.max(NumCast(layoutDoc._height), NumCast(layoutDoc._width) + 10), - _isLightbox: true, + isLightbox: true, _layout_fitWidth: true, title: StrCast(doc.title) + ' [Portal]', }), @@ -726,12 +726,12 @@ export namespace DocUtils { : { _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), borderColor: Doc.UserDoc().borderColor as string, borderWidth: Doc.UserDoc().borderWidth as number, + text_centered: BoolCast(Doc.UserDoc().textCentered), text_fitBox: BoolCast(Doc.UserDoc().fitBox), text_align: StrCast(Doc.UserDoc().textAlign), text_fontColor: StrCast(Doc.UserDoc().fontColor), @@ -752,12 +752,12 @@ export namespace DocUtils { : { _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), borderColor: Doc.UserDoc().borderColor as string, borderWidth: Doc.UserDoc().borderWidth as number, + text_centered: BoolCast(Doc.UserDoc().textCentered), text_fitBox: BoolCast(Doc.UserDoc().fitBox), text_align: StrCast(Doc.UserDoc().textAlign), text_fontColor: StrCast(Doc.UserDoc().fontColor), diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 03626107f..dd0985182 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -47,25 +47,30 @@ export enum DocumentType { JOURNAL = 'journal', // AARAV ADD } export enum CollectionViewType { - Invalid = 'invalid', + // general collections Freeform = 'freeform', - Calendar = 'calendar', Card = 'card', Carousel = 'carousel', Carousel3D = '3D Carousel', - Docking = 'docking', Grid = 'grid', - Linear = 'linear', - Map = 'map', Masonry = 'masonry', Multicolumn = 'multicolumn', Multirow = 'multirow', NoteTaking = 'notetaking', - Pile = 'pileup', Pivot = 'pivot', Schema = 'schema', Stacking = 'stacking', - StackedTimeline = 'stacked timeline', Time = 'time', Tree = 'tree', + // under development + Calendar = 'calendar', + // special collections + Docking = 'docking', + Pile = 'pileup', + StackedTimeline = 'stacked timeline', + Linear = 'linear', + Invalid = 'invalid', } + +export const specialCollectionTypes = [CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.StackedTimeline, CollectionViewType.Linear, CollectionViewType.Invalid]; +export const standardViewTypes = Object.values(CollectionViewType).filter(key => !specialCollectionTypes.includes(key)); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 668725d2b..0e3c614fe 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -4,7 +4,7 @@ import { basename } from 'path'; import { ClientUtils, OmitKeys } from '../../ClientUtils'; import { DateField } from '../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, FieldType, Opt, updateCachedAcls } from '../../fields/Doc'; -import { Initializing } from '../../fields/DocSymbols'; +import { Id } from '../../fields/FieldSymbols'; import { HtmlField } from '../../fields/HtmlField'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; @@ -18,7 +18,6 @@ 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() { @@ -43,7 +42,6 @@ export class FInfo { readOnly: boolean = false; fieldType?: FInfoFieldType; values?: FieldType[]; - onLayout?: boolean; filterable?: boolean = true; // can be used as a Filter in FilterPanel // format?: string; // format to display values (e.g, decimal places, $, etc) // parse?: ScriptField; // parse a value from a string @@ -156,12 +154,16 @@ type DROPt = DAInfo | dropActionType; type DATEt = DateInfo | number; type DTYPEt = DTypeInfo | string; export class DocumentOptions { + [key: string]: FInfo | FieldType | undefined; // coordinate and dimensions depending on view x?: NUMt = new NumInfo('horizontal coordinate in freeform view', false); y?: NUMt = new NumInfo('vertical coordinate in freeform view', false); z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, false, [1, 0]); + zIndex?: NUMt = new NumInfo('stacking index of documents in freeform view (higher numbers are towards the top'); overlayX?: NUMt = new NumInfo('horizontal coordinate in overlay view', false); overlayY?: NUMt = new NumInfo('vertical coordinate in overlay view', false); + embedContainer?: DOCt = new DocInfo('document that displays (contains) this document', false); + text?: RTFt = new RtfInfo('plain or rich text', true); text_html?: STRt = new StrInfo('plain text or html', true); _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height", false); @@ -215,11 +217,11 @@ export class DocumentOptions { author?: string; // STRt = new StrInfo('creator of document'); // bcz: don't change this. Otherwise, the userDoc's field Infos will have a FieldInfo assigned to its author field which will render it unreadable author_date?: DATEt = new DateInfo('date the document was created', true); annotationOn?: DOCt = new DocInfo('document annotated by this document', false); - _embedContainer?: DOCt = new DocInfo('document that displays (contains) this document', false); rootDocument?: DOCt = new DocInfo('document that supplies the information needed for a rendering template (eg, pres slide for PresElement)'); color?: STRt = new StrInfo('foreground color data doc', false); hidden?: BOOLt = new BoolInfo('whether the document is not rendered by its collection', false); backgroundColor?: STRt = new StrInfo('background color for data doc', false); + _override_backgroundColor?: BOOLt = new BoolInfo("whether the layout template overrides the data doc's background color"); opacity?: NUMt = new NumInfo('document opacity', false); viewTransitionTime?: NUMt = new NumInfo('transition duration for view parameters', false); dontRegisterView?: BOOLt = new BoolInfo('are views of this document registered so that they can be found when following links, etc', false); @@ -281,7 +283,6 @@ export class DocumentOptions { _layout_autoHeightMargins?: NUMt = new NumInfo('Margin heights to be added to the computed auto height of a Doc'); _layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document', false); _layout_currentTimecode?: NUMt = new NumInfo('the current timecode of a time-based document (e.g., current time of a video) value is in seconds', false); - _layout_centered?: BOOLt = new BoolInfo('whether text should be vertically centered in Doc'); _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'); @@ -307,6 +308,7 @@ export class DocumentOptions { text_fontSize?: string; text_fontFamily?: string; text_fontWeight?: string; + text_centered?: BOOLt = new BoolInfo('whether text should be vertically centered in Doc'); 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']); @@ -366,7 +368,7 @@ export class DocumentOptions { isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior'); 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'); + 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); @@ -686,10 +688,11 @@ export namespace Docs { layout: layout.view?.LayoutString(layout.dataField), }; Object.entries(options) - .filter(pair => typeof pair[1] === 'string' && pair[1].startsWith('@')) - .forEach(pair => { - if (!existing || ScriptCast(existing[pair[0]])?.script.originalScript !== pair[1].substring(1)) { - (options as { [key: string]: unknown })[pair[0]] = ComputedField.MakeFunction(pair[1].substring(1)); + .filter(([, val]) => (val as string)?.startsWith?.('@')) + .map(([key, val]) => [key, val as string]) + .forEach(([key, val]) => { + if (!existing || ScriptCast(existing[key])?.script.originalScript !== val.substring(1)) { + options[key] = ComputedField.MakeFunction(val.substring(1)); } }); return Doc.assign(existing ?? new Doc(prototypeId, true), OmitKeys(options, Object.keys(existing ?? {})).omit as { [key: string]: FieldType }, undefined, true); @@ -722,7 +725,7 @@ export namespace Docs { */ function InstanceFromProto(proto: Doc, data: FieldType | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDocIn?: Doc, noView?: boolean) { const placeholderDoc = placeholderDocIn; - const viewKeys = ['x', 'y', 'isSystem']; // keys that should be addded to the view document even though they don't begin with an "_" + const viewKeys = ['x', 'y', 'isSystem', 'overlayX', 'overlayY', 'zIndex', 'embedContainer']; // keys that should be addded to the view document even though they don't begin with an "_" const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_') as { omit: { [key: string]: FieldType | undefined }; extract: { [key: string]: FieldType | undefined } }; // dataProps.acl_Override = SharingPermissions.Unset; @@ -837,7 +840,7 @@ export namespace Docs { return TextDocument(RichTextField.textToRtf(text, img?.[Id]), { title, // _layout_autoHeight: true, - _layout_centered: true, + text_centered: true, text_align: 'center', _layout_fitWidth: true, ...opts, @@ -947,9 +950,9 @@ export namespace Docs { return InstanceFromProto( Prototypes.get(DocumentType.JOURNAL), - "", + '', { - title: "", + title: '', ...options, }, undefined, @@ -979,26 +982,22 @@ export namespace Docs { export function InkDocument(points: PointData[], options: DocumentOptions = {}, strokeWidth: number, color: string, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, isInkMask: boolean) { const ink = InstanceFromProto(Prototypes.get(DocumentType.INK), '', { title: 'ink', ...options }); - const I = Doc.GetProto(ink); - // I.layout_hideOpenButton = true; // don't show open full screen button when selected - I.color = color; - I.fillColor = fillColor && fillColor !== 'transparent' ? fillColor : undefined; - I.stroke = new InkField(points); - I.stroke_width = strokeWidth; - I.stroke_bezier = strokeBezier; - I.stroke_startMarker = arrowStart; - I.stroke_endMarker = arrowEnd; - I.stroke_dash = dash; - I.stroke_isInkMask = isInkMask; - I.text_align = 'center'; - I.rotation = 0; - I.width_min = 1; - I.height_min = 1; - I.defaultDoubleClick = 'ignore'; - I.author_date = new DateField(); - I.acl_Guest = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View; - // I.acl_Override = SharingPermissions.Unset; - I[Initializing] = false; + ink.$color = color; + ink.$fillColor = fillColor && fillColor !== 'transparent' ? fillColor : undefined; + ink.$stroke = new InkField(points); + ink.$stroke_width = strokeWidth; + ink.$stroke_bezier = strokeBezier; + ink.$stroke_startMarker = arrowStart; + ink.$stroke_endMarker = arrowEnd; + ink.$stroke_dash = dash; + ink.$stroke_isInkMask = isInkMask; + ink.$text_align = 'center'; + ink.$rotation = 0; + ink.$width_min = 1; + ink.$height_min = 1; + ink.$defaultDoubleClick = 'ignore'; + ink.$author_date = new DateField(); + ink.$acl_Guest = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View; return ink; } @@ -1073,10 +1072,6 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Linear }, id); } - export function MapCollectionDocument(documents: Array<Doc>, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Map }); - } - export function CarouselDocument(documents: Array<Doc>, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel }); } diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx index 1a5a5f889..4e321a893 100644 --- a/src/client/util/CalendarManager.tsx +++ b/src/client/util/CalendarManager.tsx @@ -236,7 +236,7 @@ export class CalendarManager extends ObservableReactComponent<object> { className="calendar-interface" style={{ background: SnappingManager.userBackgroundColor, - color: StrCast(Doc.UserDoc().userColor), + color: SnappingManager.userColor, }}> <p className="selected-doc-title" style={{ color: SnappingManager.userColor }}> <b>{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}</b> diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 537c703b4..4c492cae0 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -15,7 +15,7 @@ import { SetCachedGroups, SharingPermissions } from "../../fields/util"; import { Gestures } from "../../pen-gestures/GestureTypes"; import { DocServer } from "../DocServer"; import { DocUtils, FollowLinkScript } from '../documents/DocUtils'; -import { CollectionViewType, DocumentType } from "../documents/DocumentTypes"; +import { CollectionViewType, DocumentType, standardViewTypes } from "../documents/DocumentTypes"; import { Docs, DocumentOptions, FInfo, FInfoFieldType } from "../documents/Documents"; import { DashboardView } from "../views/DashboardView"; import { OverlayView } from "../views/OverlayView"; @@ -57,6 +57,7 @@ export interface Button { expertMode?: boolean;// available only in expert mode btnList?: List<string>; ignoreClick?: boolean; + showUntilToggle?: boolean; // whether the popup should stay open when the background is clicked. buttonText?: string; backgroundColor?: string; waitForDoubleClickToClick?: boolean; @@ -143,9 +144,9 @@ export class CurrentUserUtils { static setupNoteTemplates(doc: Doc, field="template_notes") { const tempNotes = DocCast(doc[field]); const reqdTempOpts:DocumentOptions[] = [ - { title: "Postit", backgroundColor: "yellow", icon: "sticky-note", _layout_showTitle: "title", layout_borderRounding: "5px"}, - { title: "Idea", backgroundColor: "pink", icon: "lightbulb" , _layout_showTitle: "title"}, - { title: "Topic", backgroundColor: "lightblue", icon: "book-open" , _layout_showTitle: "title"}]; + { title: "Postit", _override_backgroundColor: true, backgroundColor: "yellow", icon: "sticky-note", _layout_showTitle: "title", layout_borderRounding: "5px"}, + { title: "Idea", _override_backgroundColor: true, backgroundColor: "pink", icon: "lightbulb" , _layout_showTitle: "title"}, + { title: "Topic", _override_backgroundColor: true, backgroundColor: "lightblue", icon: "book-open" , _layout_showTitle: "title"}]; const reqdNoteList = [...reqdTempOpts.map(opts => { const reqdOpts = {...opts, isSystem:true, width:200, layout_autoHeight: true, layout_fitWidth: true}; const noteTemp = tempNotes ? DocListCast(tempNotes.data).find(fdoc => fdoc.title === opts.title): undefined; @@ -480,22 +481,24 @@ pie title Minerals in my tap water const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(this.target?.data).filter(doc => !docList(this.target.viewed).includes(doc)).length.toString())"; const getActiveDashTrails = "Doc.ActiveDashboard?.myTrails"; return [ - { title: "Dashboards", toolTip: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), ignoreClick: true, icon: "desktop", funcs: {hidden: "IsNoviceMode()"} }, - { title: "Search", toolTip: "Search ⌘F", target: this.setupSearcher(doc, "mySearcher"), ignoreClick: true, icon: "search", }, - { title: "Files", toolTip: "Files", target: this.setupFilesystem(doc, "myFilesystem"), ignoreClick: true, icon: "folder-open", }, - { title: "Tools", toolTip: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), ignoreClick: true, icon: "wrench", }, - { title: "Imports", toolTip: "Imports ⌘I", target: this.setupImportSidebar(doc, "myImports"), ignoreClick:false, icon: "upload", }, - { title: "Closed", toolTip: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), ignoreClick: true, icon: "archive", hidden: true }, // this doc is hidden from the Sidebar, but it's still being used in MyFilesystem which ignores the hidden field - { title: "Shared", toolTip: "Shared Docs", target: Doc.MySharedDocs, ignoreClick: true, icon: "users", funcs: {badgeValue: badgeValue}}, - { title: "Trails", toolTip: "Trails ⌘R", target: Doc.UserDoc(), ignoreClick: true, icon: "pres-trail", funcs: {target: getActiveDashTrails}}, - { title: "Image Grouper", toolTip: "Image Grouper", target: this.setupImageGrouper(doc, "myImageGrouper"), ignoreClick: true, icon: "folder-open", hidden: false }, - { title: "Faces", toolTip: "Unique Faces", target: this.setupFaceCollection(doc, "myFaceCollection"), ignoreClick: true, icon: "face-smile", hidden: false }, - { title: "User Doc", toolTip: "User Doc", target: this.setupUserDocView(doc, "myUserDocView"), ignoreClick: true, icon: "address-card",funcs: {hidden: "IsNoviceMode()"} }, + { title: "Dashboards", toolTip: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), icon: "desktop", funcs: {hidden: "IsNoviceMode()"} }, + { title: "Search", toolTip: "Search ⌘F", target: this.setupSearcher(doc, "mySearcher"), icon: "search", }, + { title: "Files", toolTip: "Files", target: this.setupFilesystem(doc, "myFilesystem"), icon: "folder-open", }, + { title: "Tools", toolTip: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), icon: "wrench", }, + { title: "Imports", toolTip: "Imports ⌘I", target: this.setupImportSidebar(doc, "myImports"), icon: "upload", }, + { title: "Closed", toolTip: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), icon: "archive", hidden: true }, // this doc is hidden from the Sidebar, but it's still being used in MyFilesystem which ignores the hidden field + { title: "Shared", toolTip: "Shared Docs", target: Doc.MySharedDocs, icon: "users", funcs: {badgeValue: badgeValue}}, + { title: "Trails", toolTip: "Trails ⌘R", target: Doc.UserDoc(), icon: "pres-trail", funcs: {target: getActiveDashTrails}}, + { title: "Image Grouper", toolTip: "Image Grouper", target: this.setupImageGrouper(doc, "myImageGrouper"), icon: "folder-open", hidden: false }, + { title: "Faces", toolTip: "Unique Faces", target: this.setupFaceCollection(doc, "myFaceCollection"), icon: "face-smile", hidden: false }, + { title: "User Doc", toolTip: "User Doc", target: this.setupUserDocView(doc, "myUserDocView"), icon: "address-card",funcs: {hidden: "IsNoviceMode()"} }, ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(this)'}})); } /// the empty panel that is filled with whichever left menu button's panel has been selected static setupLeftSidebarPanel(doc: Doc, field="myLeftSidebarPanel") { + const panel = DocCast(doc[field]); + if (panel) panel.proto = undefined; DocUtils.AssignDocField(doc, field, (opts) => Doc.assign(new Doc(), opts as {[key:string]: FieldType}), {title:"leftSidebarPanel", isSystem:true, undoIgnoreFields: new List<string>(['proto'])}); } @@ -601,7 +604,7 @@ pie title Minerals in my tap water const myFilesystem = DocCast(doc[field]); const newFolderOpts: DocumentOptions = { - _forceActive: true, _dragOnlyWithinContainer: true, _embedContainer: Doc.MyFilesystem, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeView_SortCriterion']), + _forceActive: true, _dragOnlyWithinContainer: true, embedContainer: Doc.MyFilesystem, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeView_SortCriterion']), title: "New folder", color: Colors.BLACK, btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true }; const newFolderScript = { onClick: CollectionTreeView.AddTreeFunc}; @@ -736,12 +739,12 @@ pie title Minerals in my tap water } static viewTools(): Button[] { return [ - { title: "Tags", icon: "id-card", toolTip: "Toggle Tags display", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"toggle-tags",funcs: { }, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} }, - { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "Fit All", icon: "object-group", toolTip:"Fit Docs to View (double tap to persist)", - btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform - { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Tags", icon: "id-card", toolTip: "Toggle Tags display", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"toggle-tags",funcs: { }, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} }, + { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Fit All", icon: "object-group", toolTip:"Fit Docs to View (double tap to persist)", + btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform + { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform ] } static textTools():Button[] { @@ -759,14 +762,14 @@ pie title Minerals in my tap water { title: "Vcenter", toolTip: "Vertical center", btnType: ButtonType.ToggleButton, icon: "pallet", toolType:"vcent", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment",ignoreClick: true, subMenu: [ - { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, - { 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: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, + { 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()'}}, + { 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()'}}, // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}}, // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}}, // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}}, @@ -778,7 +781,7 @@ pie title Minerals in my tap water { 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_);}' }, + { title: "Ink", toolTip: "Ink", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Ink, showUntilToggle: true, 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_);}' }}, @@ -786,7 +789,7 @@ pie title Minerals in my tap water ]}, { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: InkProperty.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: InkProperty.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_);}' }, + { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, showUntilToggle: true, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, subMenu: [ { 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 between intersections",btnType:ButtonType.ToggleButton,icon:"xmark", toolType:InkEraserTool.Segment, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}}, @@ -824,36 +827,33 @@ pie title Minerals in my tap water { title: "Rotate",toolTip: "Rotate 90", btnType: ButtonType.ClickButton, icon: "redo-alt", scripts: { onClick: 'imageRotate90();' }}, ]; } - static contextMenuTools(doc:Doc):Button[] { + static contextMenuTools():Button[] { return [ - { 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.Pivot, 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, shiftKey, _readOnly_); }'}}, + { title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, expertMode: false, btnList: new List<string>(standardViewTypes), ignoreClick: true, width: 100, scripts: { script: '{ return setView(value, shiftKey, _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_)'} }, // Only when a document is selected - { title: "Border", icon: "pen", toolTip: "Border Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBorderColor(value, _readOnly_)'} }, // Only when a document is selected + { title: "Border", icon: "border-style", toolTip: "Border Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBorderColor(value, _readOnly_)'} }, // Only when a document is selected { title: "B.Width", toolTip: "Border width", btnType: ButtonType.NumberSliderButton, ignoreClick: true, scripts: {script: '{ return setBorderWidth(value, _readOnly_);}'}, numBtnMin: 0, linearBtnWidth:40}, { 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: "Chat", icon: "lightbulb", toolTip: "Toggle Chat Assistant",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat", funcs: {}, width: 30, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} }, - { title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.filterTools(), ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string}, - { title: "Sort", icon: "Sort", toolTip: "Sort Documents", subMenu: CurrentUserUtils.sortTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available - { 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 - { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available - { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available - { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available - { title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: CurrentUserUtils.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available - - { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected - { title: "Video", icon: "Video", toolTip: "Video functions", subMenu: CurrentUserUtils.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected - { title: "Image", icon: "Image", toolTip: "Image functions", subMenu: CurrentUserUtils.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected - { title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, linearBtnWidth:58 }, // Only when Schema is selected + { title: "Filter", icon: "=", toolTip: "Filter cards by tags", btnType: ButtonType.MultiToggleButton, + subMenu: this.filterTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},ignoreClick:true, width: 30}, + { title: "Sort", icon: "Sort", toolTip: "Sort Documents", subMenu: this.sortTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: this.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: this.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available + { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: this.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "View", icon: "View", toolTip: "View tools", subMenu: this.viewTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: this.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available + + { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: this.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected + { title: "Video", icon: "Video", toolTip: "Video functions", subMenu: this.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected + { title: "Image", icon: "Image", toolTip: "Image functions", subMenu: this.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected + { title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: this.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, linearBtnWidth:58 }, // Only when Schema is selected ]; } @@ -867,7 +867,7 @@ pie title Minerals in my tap water _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth, toolType: params.toolType, expertMode: params.expertMode, _dragOnlyWithinContainer: true, _lockedPosition: true, - _embedContainer: btnContainer + embedContainer: btnContainer }; const reqdFuncs:{[key:string]:string} = { ...params.funcs, @@ -899,7 +899,7 @@ pie title Minerals in my tap water static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") { const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: dropActionType.embed, childDontRegisterViews: true, linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 }; const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined); - const ctxtMenuBtns = CurrentUserUtils.contextMenuTools(doc).map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) ); + const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) ); return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns); } /// Initializes all the default buttons for the top bar context menu diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 44fbda319..2eef3da0e 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -356,23 +356,16 @@ export namespace DictationManager { action: (target: DocumentView, matches: RegExpExecArray) => { const count = interpretNumber(matches[1]); const what = matches[2]; - const dataDoc = Doc.GetProto(target.Document); - const fieldKey = 'data'; - if (isNaN(count)) { - return; - } - for (let i = 0; i < count; i++) { - let created: Doc | undefined; - switch (what) { - case 'image': - created = Docs.Create.ImageDocument('https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg'); - break; - case 'nested collection': - created = Docs.Create.FreeformDocument([], {}); - break; - default: + if (!isNaN(count)) { + for (let i = 0; i < count; i++) { + const created = (() => { + switch (what) { + case 'image': return Docs.Create.ImageDocument('https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg'); + case 'nested collection':return Docs.Create.FreeformDocument([], {}); + } // prettier-ignore + })(); + created && Doc.AddDocToList(target.dataDoc, Doc.LayoutFieldKey(target.Document), created); } - created && Doc.AddDocToList(dataDoc, fieldKey, created); } }, restrictTo: [DocumentType.COL], @@ -388,13 +381,15 @@ export namespace DictationManager { }, ]; } - export function recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) { + export function recordAudioAnnotation(doc: Doc, fieldIn: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) { + const field = '$' + fieldIn + '_audioAnnotations'; let gumStream: MediaStream | undefined; let recorder: MediaRecorder | undefined; navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { - let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null); + let audioTextAnnos = Cast(doc[field + '_text'], listSpec('string'), null); if (audioTextAnnos) audioTextAnnos.push(''); - else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']); + else audioTextAnnos = doc[field + '_text'] = new List<string>(['']); + doc._layout_showTags = true; DictationManager.Controls.listen({ interimHandler: value => { audioTextAnnos[audioTextAnnos.length - 1] = value; }, // prettier-ignore continuous: { indefinite: false }, @@ -415,16 +410,16 @@ export namespace DictationManager { const [{ result }] = await Networking.UploadFilesToServer({ file: file as Blob & { name: string; lastModified: number; webkitRelativePath: string } }); if (!(result instanceof Error)) { const audioField = new AudioField(result.accessPaths.agnostic.client); - const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null); + const audioAnnos = Cast(doc[field], listSpec(AudioField), null); if (audioAnnos) audioAnnos.push(audioField); - else dataDoc[field + '_audioAnnotations'] = new List([audioField]); + else doc[field] = new List([audioField]); } }; recorder.start(); const stopFunc = () => { recorder?.stop(); DictationManager.Controls.stop(/* false */); - dataDoc.audioAnnoState = AudioAnnoState.stopped; + doc._audioAnnoState = AudioAnnoState.stopped; gumStream?.getAudioTracks()[0].stop(); }; if (onRecording) onRecording(stopFunc); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index b5d29be4c..7d3f63448 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -1,5 +1,5 @@ import { Doc, DocListCast, StrListCast } from '../../fields/Doc'; -import { DocData } from '../../fields/DocSymbols'; +import { DocData, DocLayout } from '../../fields/DocSymbols'; import { ObjectField } from '../../fields/ObjectField'; import { RichTextField } from '../../fields/RichTextField'; import { ComputedField, ScriptField } from '../../fields/ScriptField'; @@ -93,7 +93,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data?.draggedDocuments.forEach((doc, i) => { let dbox = doc; // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant - if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes(FontIconBox.name)) { + if (doc.type === DocumentType.FONTICON || StrCast(doc[DocLayout].layout).includes(FontIconBox.name)) { if (data.dropPropertiesToRemove || dbox.dropPropertiesToRemove) { // dbox = Doc.MakeEmbedding(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon dbox = Doc.MakeEmbedding(dbox); diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 1ec85c9d9..79d009720 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -19,6 +19,7 @@ import './GroupManager.scss'; import { GroupMemberView } from './GroupMemberView'; import { SharingManager, User } from './SharingManager'; import { SnappingManager } from './SnappingManager'; +import { SettingsManager } from './SettingsManager'; /** * Interface for options for the react-select component @@ -290,11 +291,12 @@ export class GroupManager extends ObservableReactComponent<object> { this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false; })} - color={StrCast(Doc.UserDoc().userColor)} + color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} /> </div> </div> - <div className="group-input" style={{ border: StrCast(Doc.UserDoc().userColor) }}> + <div className="group-input" style={{ border: SettingsManager.userColor }}> <input ref={this.inputRef} onKeyDown={this.handleKeyDown} @@ -306,7 +308,7 @@ export class GroupManager extends ObservableReactComponent<object> { })} /> </div> - <div style={{ border: StrCast(Doc.UserDoc().userColor) }}> + <div style={{ border: SettingsManager.userColor }}> <Select className="select-users" isMulti @@ -332,15 +334,15 @@ export class GroupManager extends ObservableReactComponent<object> { }), valueContainer: () => ({ display: 'inline-flex', - fontStyle: StrCast(Doc.UserDoc().userColor), - color: StrCast(Doc.UserDoc().userColor), + fontStyle: SettingsManager.userColor, + color: SettingsManager.userColor, width: '100%', }), }} /> </div> <div className="create-button"> - <Button text="Create" type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={this.createGroup} /> + <Button text="Create" type={Type.TERT} color={SettingsManager.userColor} background={SettingsManager.userBackgroundColor} onClick={this.createGroup} /> </div> </div> ); diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index b0886a67c..2fa6e3059 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -51,6 +51,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: ts const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error).filter(diag => // diag.code !== 2304 && diag.code !== 2339 && + diag.code !== 2314 && (diag.code !== 2552 ||!Object.keys(scriptingGlobals).includes(diagnostics[0].messageText.toString().match(/Cannot find name '([A-Za-z0-9$-_]+)'/)?.[1]??"-------")) ); // prettier-ignore if ((options.typecheck !== false && errors.length) || !script) { diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 6ea242fc3..89e3686b2 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -135,9 +135,9 @@ export class SettingsManager extends React.Component<object> { reaction( () => [SettingsManager.userBackgroundColor, SettingsManager.userColor, SettingsManager.userVariantColor], ([back, user, variant]) => { - SnappingManager.userBackgroundColor = back; - SnappingManager.userVariantColor = variant; - SnappingManager.userColor = user; + SnappingManager.SetUserBackgroundColor(back); + SnappingManager.SetUserVariantColor(variant); + SnappingManager.SetUserColor(user); }, { fireImmediately: true } ); @@ -177,6 +177,7 @@ export class SettingsManager extends React.Component<object> { }))} dropdownType={DropdownType.SELECT} color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} fillWidth /> <Toggle formLabel="Match System" toggleType={ToggleType.SWITCH} color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.UserDoc().userThemeSystem)} onClick={this.userThemeSystemToggle} /> @@ -300,6 +301,7 @@ export class SettingsManager extends React.Component<object> { <NumberDropdown number={NumCast(Doc.UserDoc().headerHeight, 30)} color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} numberDropdownType="slider" min={6} max={60} @@ -342,6 +344,7 @@ export class SettingsManager extends React.Component<object> { <Group formLabel="Default Font"> <NumberDropdown color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} numberDropdownType="slider" min={0} max={50} @@ -355,10 +358,10 @@ export class SettingsManager extends React.Component<object> { /> <ColorPicker color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} 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" @@ -390,6 +393,7 @@ export class SettingsManager extends React.Component<object> { this.changeFontFamily(val as string); }} color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} fillWidth /> </Group> @@ -402,12 +406,12 @@ export class SettingsManager extends React.Component<object> { @computed get passwordContent() { return ( <div className="password-content"> - <EditableText placeholder="Current password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'curr')} fillWidth password /> - <EditableText placeholder="New password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'new')} fillWidth password /> - <EditableText placeholder="Confirm new password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'conf')} fillWidth password /> + <EditableText placeholder="Current password" type={Type.SEC} color={SettingsManager.userColor} background={SettingsManager.userBackgroundColor} val="" setVal={val => this.changeVal(val as string, 'curr')} fillWidth password /> + <EditableText placeholder="New password" type={Type.SEC} color={SettingsManager.userColor} background={SettingsManager.userBackgroundColor} val="" setVal={val => this.changeVal(val as string, 'new')} fillWidth password /> + <EditableText placeholder="Confirm new password" type={Type.SEC} color={SettingsManager.userColor} background={SettingsManager.userBackgroundColor} val="" setVal={val => this.changeVal(val as string, 'conf')} fillWidth password /> {!this._passwordResultText ? null : <div className={`${this._passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this._passwordResultText}</div>} - <Button type={Type.SEC} text="Forgot Password" color={SettingsManager.userColor} /> - <Button type={Type.TERT} text="Submit" onClick={this.changePassword} color={SettingsManager.userColor} /> + <Button type={Type.SEC} text="Forgot Password" color={SettingsManager.userColor} background={SettingsManager.userBackgroundColor} /> + <Button type={Type.TERT} text="Submit" onClick={this.changePassword} color={SettingsManager.userColor} background={SettingsManager.userBackgroundColor} /> </div> ); } @@ -415,7 +419,7 @@ export class SettingsManager extends React.Component<object> { @computed get accountOthersContent() { return ( <div className="account-others-content"> - <Button type={Type.TERT} text="Connect to Google" iconPlacement="left" icon={<BsGoogle />} onClick={() => this.googleAuthorize()} /> + <Button type={Type.TERT} text="Connect to Google" color={SnappingManager.userColor} background={SnappingManager.userBackgroundColor} iconPlacement="left" icon={<BsGoogle />} onClick={() => this.googleAuthorize()} /> </div> ); } @@ -465,6 +469,7 @@ export class SettingsManager extends React.Component<object> { type={Type.TERT} placement="bottom-start" color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} fillWidth /> <Toggle formLabel="Playground Mode" toggleType={ToggleType.SWITCH} toggleStatus={this._playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} /> @@ -494,13 +499,14 @@ export class SettingsManager extends React.Component<object> { type={Type.TERT} placement="bottom-start" color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} /> </div> </div> <div className="tab-column"> <div className="tab-column-title">Permissions</div> <div className="tab-column-content"> - <Button text="Manage Groups" type={Type.TERT} onClick={() => GroupManager.Instance?.open()} color={SettingsManager.userColor} /> + <Button text="Manage Groups" type={Type.TERT} color={SnappingManager.userColor} background={SnappingManager.userBackgroundColor} onClick={() => GroupManager.Instance?.open()} /> <Toggle toggleType={ToggleType.SWITCH} formLabel="Default access private" diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 962f51cd4..db8c6c5cb 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -286,7 +286,7 @@ export class SharingManager extends React.Component<object> { {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')} </p> <div className="share-copy-link"> - <Button type={Type.TERT} color={SnappingManager.userColor} icon={<FontAwesomeIcon icon="copy" size="sm" />} iconPlacement="left" text="Copy Guest URL" onClick={this.copyURL} /> + <Button type={Type.TERT} color={SnappingManager.userColor} background={SnappingManager.userBackgroundColor} icon={<FontAwesomeIcon icon="copy" size="sm" />} iconPlacement="left" text="Copy Guest URL" onClick={this.copyURL} /> </div> <div className="close-button"> <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SnappingManager.userColor} /> @@ -331,7 +331,7 @@ export class SharingManager extends React.Component<object> { </select> </div> <div className="share-button"> - <Button text="SHARE" type={Type.TERT} color={SnappingManager.userColor} onClick={this.share} /> + <Button text="SHARE" type={Type.TERT} color={SnappingManager.userColor} background={SnappingManager.userBackgroundColor} onClick={this.share} /> </div> </div> <div className="sort-checkboxes"> diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index 3bbc297b8..54c91087f 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -1,4 +1,4 @@ -import { observable, action, runInAction, makeObservable } from 'mobx'; +import { observable, action, makeObservable } from 'mobx'; import { Gestures } from '../../pen-gestures/GestureTypes'; export enum freeformScrollMode { @@ -34,6 +34,9 @@ export class SnappingManager { @observable _keepGestureMode: boolean = false; // for whether primitive selection enters a one-shot or persistent mode @observable _inkShape: Gestures | undefined = undefined; @observable _chatVisible: boolean = false; + @observable _userBackgroundColor: string | undefined = undefined; + @observable _userVariantColor: string | undefined = undefined; + @observable _userColor: string | undefined = undefined; private constructor() { SnappingManager._manager = this; @@ -48,6 +51,9 @@ export class SnappingManager { this.Instance._vertSnapLines.push(...vertLines); }; + public static get userBackgroundColor() { return this.Instance._userBackgroundColor; } // prettier-ignore + public static get userVariantColor() { return this.Instance._userVariantColor; } // prettier-ignore + public static get userColor() { return this.Instance._userColor; } // prettier-ignore public static get HorizSnapLines() { return this.Instance._horizSnapLines; } // prettier-ignore public static get VertSnapLines() { return this.Instance._vertSnapLines; } // prettier-ignore public static get LongPress() { return this.Instance._longPress; } // prettier-ignore @@ -71,29 +77,29 @@ export class SnappingManager { public static get InkShape() { return this.Instance._inkShape; } // prettier-ignore public static get ChatVisible() { return this.Instance._chatVisible; } // 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 - public static SetCtrlKey = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore - public static SetMetaKey = (down: boolean) => runInAction(() => {this.Instance._metaKey = down}); // prettier-ignore - public static SetHideUI = (vis: boolean) => runInAction(() => {this.Instance._hideUI = vis}); // prettier-ignore - public static SetShowPresPaths = (paths:boolean) => runInAction(() => {this.Instance._showPresPaths = paths}); // prettier-ignore - public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => {this.Instance._isLinkFollowing = follow}); // prettier-ignore - public static SetIsDragging = (drag: boolean) => runInAction(() => {this.Instance._isDragging = drag}); // prettier-ignore - public static SetIsResizing = (docid?:string) => runInAction(() => {this.Instance._isResizing = docid}); // prettier-ignore - public static SetCanEmbed = (embed:boolean) => runInAction(() => {this.Instance._canEmbed = embed}); // prettier-ignore - public static SetExploreMode = (state:boolean) => runInAction(() => {this.Instance._exploreMode = state}); // prettier-ignore - public static TriggerUserPanned = () => runInAction(() => {this.Instance._userPanned = !this.Instance._userPanned}); // prettier-ignore - public static SetServerVersion = (version:string) =>runInAction(() => {this.Instance._serverVersion = version}); // prettier-ignore - public static SetLastPressedBtn = (id:string) =>runInAction(() => {this.Instance._lastBtnId = id}); // prettier-ignore - 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 SetChatVisible = (vis:boolean) =>runInAction(() => {this.Instance._chatVisible = vis}); // prettier-ignore + public static SetUserBackgroundColor = action((color: string) => (this.Instance._userBackgroundColor = color)); // prettier-ignore + public static SetUserVariantColor = action((color: string) => (this.Instance._userVariantColor = color)); // prettier-ignore + public static SetUserColor = action((color: string) => (this.Instance._userColor = color)); // prettier-ignore + public static SetLongPress = action((press: boolean)=> (this.Instance._longPress = press)); // prettier-ignore + public static SetShiftKey = action((down: boolean) => (this.Instance._shiftKey = down)); // prettier-ignore + public static SetCtrlKey = action((down: boolean) => (this.Instance._ctrlKey = down)); // prettier-ignore + public static SetMetaKey = action((down: boolean) => (this.Instance._metaKey = down)); // prettier-ignore + public static SetHideUI = action((vis: boolean) => (this.Instance._hideUI = vis)); // prettier-ignore + public static SetShowPresPaths = action((paths:boolean) => (this.Instance._showPresPaths = paths)); // prettier-ignore + public static SetIsLinkFollowing = action((follow:boolean)=> (this.Instance._isLinkFollowing = follow)); // prettier-ignore + public static SetIsDragging = action((drag: boolean) => (this.Instance._isDragging = drag)); // prettier-ignore + public static SetIsResizing = action((docid?:string) => (this.Instance._isResizing = docid)); // prettier-ignore + public static SetCanEmbed = action((embed:boolean) => (this.Instance._canEmbed = embed)); // prettier-ignore + public static SetExploreMode = action((state:boolean) => (this.Instance._exploreMode = state)); // prettier-ignore + public static TriggerUserPanned = action(() => (this.Instance._userPanned = !this.Instance._userPanned)); // prettier-ignore + public static SetServerVersion = action((version:string)=> (this.Instance._serverVersion = version)); // prettier-ignore + public static SetLastPressedBtn = action((id:string) => (this.Instance._lastBtnId = id)); // prettier-ignore + public static SetPropertiesWidth = action((wid:number) => (this.Instance._propertyWid = wid)); // prettier-ignore + public static SetPrintToConsole = action((state:boolean) => (this.Instance._printToConsole = state)); // prettier-ignore + public static SetHideDecorations = action((state:boolean) => (this.Instance._hideDecorations = state)); // prettier-ignore + public static SetKeepGestureMode = action((state:boolean) => (this.Instance._keepGestureMode = state)); // prettier-ignore + public static SetInkShape = action((shape?:Gestures)=>(this.Instance._inkShape = shape)); // prettier-ignore + public static SetChatVisible = action((vis:boolean) => (this.Instance._chatVisible = vis)); // prettier-ignore - public static userColor: string | undefined; - public static userVariantColor: string | undefined; - public static userBackgroundColor: string | undefined; public static SettingsStyle: CSSStyleSheet | null; } diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index 99dee6410..b5e56cad5 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -4,6 +4,7 @@ import { SnappingManager } from '../util/SnappingManager'; import './AntimodeMenu.scss'; import { ObservableReactComponent } from './ObservableReactComponent'; +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface AntimodeMenuProps {} /** @@ -162,6 +163,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends Observab transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, position: this.Pinned ? 'unset' : undefined, + border: `${SnappingManager.userColor} solid 1px`, }}> {buttons} </div> diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 8f254ddcd..45c80c6f7 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { returnFalse } from '../../ClientUtils'; import { DateField } from '../../fields/DateField'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData } from '../../fields/DocSymbols'; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData, DocLayout } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { DocCast, toList } from '../../fields/Types'; import { GetEffectiveAcl, inheritParentAcls } from '../../fields/util'; @@ -19,6 +19,7 @@ import { FieldViewProps } from './nodes/FieldView'; * */ export interface DocComponentProps { Document: Doc; + TemplateDataDocument?: Doc; LayoutTemplate?: () => Opt<Doc>; LayoutTemplateString?: string; } @@ -36,7 +37,7 @@ export function DocComponent<P extends DocComponentProps>() { * NOTE: it is very unlikely that you really want to use this method. Instead * consider: Document, layoutDoc, dataDoc */ - get _renderDoc() { + get Document() { return this._props.Document; } @@ -46,8 +47,15 @@ export function DocComponent<P extends DocComponentProps>() { * necessarily the Doc being rendered in the current React component. * This Doc inherits from the dataDoc, and may or may not inherit (or be) the layoutDoc. */ - get Document() { - return DocCast(this._renderDoc.rootDocument, this._renderDoc); + get rootDoc() { + return DocCast(this.Document.rootDocument, this.Document); + } + + /** + * Whether the doc is a sub-componentn of a compound template doc. + */ + get isTemplateForField() { + return this.rootDoc !== this.layoutDoc && this._props.TemplateDataDocument; } /** * This is the document being rendered by the React component. In the @@ -56,14 +64,14 @@ export function DocComponent<P extends DocComponentProps>() { * This may or may not inherit from the data doc. */ @computed get layoutDoc() { - return this._props.LayoutTemplateString ? this._renderDoc : Doc.Layout(this._renderDoc, this._props.LayoutTemplate?.()); + return this._props.LayoutTemplateString ? this.Document : Doc.Layout(this.Document, this._props.LayoutTemplate?.()); } /** * This is the unique data repository for a document that stores the intrinsic document data. */ @computed get dataDoc() { - return this._renderDoc[DocData]; + return this.Document[DocData]; } } return Component; @@ -92,10 +100,8 @@ export function ViewBoxBaseComponent<P extends FieldViewProps>() { * This is the doc that is being rendered. It will be either: * 1) the same as Document if the root of a regular or compound Doc is rendered * 2) the same as the layoutDoc if a component of a compound Doc is rendered. - * NOTE: it is very unlikely that you really want to use this method. Instead - * consider: Document, layoutDoc, dataDoc */ - get _renderDoc() { + get Document() { return this._props.Document; } @@ -104,9 +110,12 @@ export function ViewBoxBaseComponent<P extends FieldViewProps>() { * this is the outermost Doc that represents the entire compound Doc. It is not * necessarily the Doc being rendered in the current React component. * This Doc inherits from the dataDoc, and may or may not inherit (or be) the layoutDoc. + * + * NOTE: it is very unlikely that you really want to use this method. Instead + * consider: Document, layoutDoc, dataDoc */ - get Document() { - return DocCast(this._renderDoc.rootDocument, this._renderDoc); + get rootDoc() { + return DocCast(this.Document.rootDocument, this.Document); } /** * This is the document being rendered by the React component. In the @@ -115,14 +124,14 @@ export function ViewBoxBaseComponent<P extends FieldViewProps>() { * This may or may not inherit from the data doc. */ @computed get layoutDoc() { - return Doc.Layout(this._renderDoc); + return this.Document[DocLayout]; } /** * This is the unique data repository for a dcoument that stores the intrinsic document data */ @computed get dataDoc() { - return this._renderDoc.isTemplateForField || this._renderDoc.isTemplateDoc ? (this._props.TemplateDataDocument ?? this._renderDoc[DocData]) : this._renderDoc[DocData]; + return this.Document.isTemplateForField || this.Document.isTemplateDoc ? (this._props.TemplateDataDocument ?? this.Document[DocData]) : this.Document[DocData]; } /** @@ -165,10 +174,8 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { * This is the doc that is being rendered. It will be either: * 1) the same as Document if the root of a regular or compound Doc is rendered * 2) the same as the layoutDoc if a component of a compound Doc is rendered. - * NOTE: it would unlikely that you really want to use this instead of the - * other Doc options (Document, layoutDoc, dataDoc) */ - get _renderDoc() { + get Document() { return this._props.Document; } @@ -177,22 +184,27 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { * this is the outermost Doc that represents the entire compound Doc. It is not * necessarily the Doc being rendered in the current React component. * This Doc inherits from the dataDoc, and may or may not inherit (or be) the layoutDoc. + * + * NOTE: it would unlikely that you really want to use this instead of the + * other Doc options (Document, layoutDoc, dataDoc) */ - @computed get Document() { - return DocCast(this._renderDoc.rootDocument, this._renderDoc); + @computed get rootDoc() { + return DocCast(this.Document.rootDocument, this.Document); } /** * This is the document being rendered. It may be a template so it may or may no inherit from the data doc. */ @computed get layoutDoc() { - return Doc.Layout(this._renderDoc); + return this.Document[DocLayout]; } /** * This is the unique data repository for a dcoument that stores the intrinsic document data */ @computed get dataDoc() { - return this._renderDoc.isTemplateForField || this._renderDoc.isTemplateDoc ? (this._props.TemplateDataDocument ?? this._renderDoc[DocData]) : this._props.Document[DocData]; + return this.Document.isTemplateForField || this.Document.isTemplateDoc ? + (this._props.TemplateDataDocument ?? this.Document[DocData]) : + this.Document[DocData]; // prettier-ignore } /** diff --git a/src/client/views/DocViewUtils.ts b/src/client/views/DocViewUtils.ts index 1f5f29c7e..5710187b5 100644 --- a/src/client/views/DocViewUtils.ts +++ b/src/client/views/DocViewUtils.ts @@ -1,6 +1,3 @@ -/* eslint-disable prefer-destructuring */ -/* eslint-disable default-param-last */ -/* eslint-disable no-use-before-define */ import { runInAction } from 'mobx'; import { Doc, SetActiveAudioLinker } from '../../fields/Doc'; import { DocUtils } from '../documents/DocUtils'; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index b17dbc93d..9a57a5e6c 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -358,7 +358,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( view => view && DictationManager.recordAudioAnnotation( - view.dataDoc, + view.Document, view.LayoutFieldKey, stopFunc => { this._stopFunc = stopFunc; diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index a5afb1305..09a13634f 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -40,9 +40,10 @@ $resizeHandler: 8px; } .documentDecorations-tagsView { position: absolute; - height: 100%; + width: 100%; pointer-events: all; border-radius: 50%; + top: 30; // offset by height of documentButtonBar so that items can be clicked without overlap interference color: black; } } @@ -91,30 +92,71 @@ $resizeHandler: 8px; } } - .documentDecorations-closeButton { + .documentDecorations-closeWrapper { display: flex; - align-items: center; - justify-content: center; - background: #fb9d75; - border: solid 1.5px #a94442; - color: #fb9d75; - transition: 0.1s ease; - opacity: 1; - pointer-events: all; - width: 20px; - height: 20px; - min-width: 20px; - border-radius: 100%; - opacity: 0.5; - cursor: pointer; - - &:hover { - color: #a94442; + .documentDecorations-closeButton { + display: flex; + align-items: center; + justify-content: center; + background: #fb9d75; + border: solid 1.5px #a94442; + color: #fb9d75; + transition: 0.1s ease; opacity: 1; + pointer-events: all; + width: 20px; + height: 20px; + min-width: 20px; + border-radius: 100%; + opacity: 0.5; + cursor: pointer; + + &:hover { + color: #a94442; + opacity: 1; + width: 20px; + min-width: 20px; + } + + > svg { + margin: 0; + } } - > svg { - margin: 0; + .documentDecorations-minimizeButton { + display: none; + align-items: center; + justify-content: center; + background: #ffdd00; + border: solid 1.5px #a94442; + color: #ffdd00; + transition: 0.1s ease; + font-size: 11px; + opacity: 1; + grid-column: 2; + pointer-events: all; + width: 20px; + height: 20px; + min-width: 20px; + border-radius: 100%; + opacity: 0.5; + cursor: pointer; + + &:hover { + color: #a94442; + opacity: 1; + } + + > svg { + margin: 0; + } + } + &:hover { + width: 40px; + min-width: 40px; + .documentDecorations-minimizeButton { + display: flex; + } } } @@ -145,38 +187,9 @@ $resizeHandler: 8px; } } - .documentDecorations-minimizeButton { - display: flex; - align-items: center; - justify-content: center; - background: #ffdd00; - border: solid 1.5px #a94442; - color: #ffdd00; - transition: 0.1s ease; - font-size: 11px; - opacity: 1; - grid-column: 2; - pointer-events: all; - width: 20px; - height: 20px; - min-width: 20px; - border-radius: 100%; - opacity: 0.5; - cursor: pointer; - - &:hover { - color: #a94442; - opacity: 1; - } - - > svg { - margin: 0; - } - } - .documentDecorations-title { opacity: 1; - width: calc(100% - 60px); // = margin-left + margin-right + width: 100%; grid-column: 3; pointer-events: auto; overflow: hidden; @@ -315,7 +328,7 @@ $resizeHandler: 8px; .documentDecorations-rightResizer { pointer-events: auto; background: global.$medium-gray-dim; - //opacity: 0.2; + opacity: 0.2; &:hover { opacity: 1; } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 19b987cb5..0756d3d9e 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -49,7 +49,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora static Instance: DocumentDecorations; private _resizeHdlId = ''; private _keyinput = React.createRef<HTMLInputElement>(); - private _resizeBorderWidth = 16; + private _resizeBorderWidth = 8; private _linkBoxHeight = 20 + 3; // link button height + margin private _titleHeight = 20; private _resizeUndo?: UndoManager.Batch; @@ -98,11 +98,11 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora const bounds = { ...this.Bounds }; const leftBounds = this._props.boundsLeft; const topBounds = DocumentView.LightboxDoc() ? 0 : this._props.boundsTop; - bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; - bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; + bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth) + this._resizeBorderWidth; + bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth - this._titleHeight) + this._resizeBorderWidth + this._titleHeight; const borderRadiusDraggerWidth = 15; - bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); - bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); + bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth) - this._resizeBorderWidth - borderRadiusDraggerWidth)); + bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth + this._linkBoxHeight) - this._resizeBorderWidth - this._linkBoxHeight)); return bounds; } @@ -320,7 +320,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); const dist = moveEv.clientX < x && moveEv.clientY < y ? 0 : Math.sqrt((moveEv.clientX - x) * (moveEv.clientX - x) + (moveEv.clientY - y) * (moveEv.clientY - y)); DocumentView.SelectedDocs().forEach(doc => { - const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2); + const docMax = Math.min(NumCast(doc._width) / 2, NumCast(doc._height) / 2); const radius = Math.min(1, dist / maxDist) * docMax; // set radius based on ratio of drag distance to half diagonal distance of bounding box doc._layout_borderRounding = `${radius}px`; }); @@ -388,7 +388,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora DocumentView.Selected().forEach(dv => { const accumRot = (NumCast(dv.Document._rotation) / 180) * Math.PI; const localRotCtr = dv.screenToViewTransform().transformPoint(rcScreen.X, rcScreen.Y); - const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.Document.width) / 2, localRotCtr[1] - NumCast(dv.Document.height) / 2]; + const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.Document._width) / 2, localRotCtr[1] - NumCast(dv.Document._height) / 2]; const startRotCtr = Utils.rotPt(localRotCtrOffset[0], localRotCtrOffset[1], -accumRot); const unrotatedDocPos = { x: NumCast(dv.Document.x) + localRotCtrOffset[0] - startRotCtr.x, y: NumCast(dv.Document.y) + localRotCtrOffset[1] - startRotCtr.y }; infos.set(dv.Document, { unrotatedDocPos, startRotCtr, accumRot }); @@ -623,8 +623,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => { doc.$data = new InkField(inkPts.map( (ipt) => ({// (new x — oldx) + newWidth * (oldxpoint /oldWidth) - X: NumCast(doc.x) - x + (NumCast(doc.width) * ipt.X) / width, - Y: NumCast(doc.y) - y + (NumCast(doc.height) * ipt.Y) / height, + X: NumCast(doc.x) - x + (NumCast(doc._width) * ipt.X) / width, + Y: NumCast(doc.y) - y + (NumCast(doc._height) * ipt.Y) / height, }))); // prettier-ignore Doc.SetNativeWidth(doc, undefined); Doc.SetNativeHeight(doc, undefined); @@ -635,7 +635,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora if (DocumentView.Selected().length === 1) { const selected = DocumentView.Selected()[0]; if (this._titleControlString.startsWith('$')) { - return Field.toJavascriptString(selected._renderDoc[this._titleControlString.substring(1)] as FieldType) || '-unset-'; + return Field.toJavascriptString(selected.Document[this._titleControlString.substring(1)] as FieldType) || '-unset-'; } return this._accumulatedTitle; } @@ -719,7 +719,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora // Radius constants const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView; const borderRadius = numberValue(Cast(seldocview.Document.layout_borderRounding, 'string', null)); - const docMax = Math.min(NumCast(seldocview.Document.width) / 2, NumCast(seldocview.Document.height) / 2); + const docMax = Math.min(NumCast(seldocview.Document._width) / 2, NumCast(seldocview.Document._height) / 2); const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); const radiusHandle = (borderRadius / docMax) * maxDist; const radiusHandleLocation = Math.min(radiusHandle, maxDist); @@ -773,7 +773,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora </span> )} {sharingMenu} - {!useLock ? null : ( + {!useLock || hideTitle ? null : ( <Tooltip key="lock" title={<div className="dash-tooltip">toggle ability to interact with document</div>} placement="top"> <div className="documentDecorations-lock" style={{ color: seldocview.Document._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown}> <FontAwesomeIcon size="sm" icon="lock" /> @@ -790,10 +790,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora <div className="documentDecorations-background" style={{ - width: bounds.r - bounds.x + this._resizeBorderWidth + 'px', - height: bounds.b - bounds.y + this._resizeBorderWidth + 'px', - left: bounds.x - this._resizeBorderWidth / 2, - top: bounds.y - this._resizeBorderWidth / 2, + width: bounds.r - bounds.x + 2 * this._resizeBorderWidth + 'px', + height: bounds.b - bounds.y + 2 * this._resizeBorderWidth + 'px', + left: bounds.x - this._resizeBorderWidth, + top: bounds.y - this._resizeBorderWidth, transformOrigin, background: SnappingManager.ShiftKey ? undefined : 'yellow', pointerEvents: SnappingManager.ShiftKey || SnappingManager.IsResizing ? 'none' : 'all', @@ -807,10 +807,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora <div className={`documentDecorations-container ${this._showNothing ? 'showNothing' : ''}`} style={{ - transform: `translate(${bounds.x - this._resizeBorderWidth / 2}px, ${bounds.y - this._resizeBorderWidth / 2 - this._titleHeight}px) rotate(${rotation}deg)`, + transform: `translate(${bounds.x - this._resizeBorderWidth}px, ${bounds.y - this._resizeBorderWidth - this._titleHeight}px) rotate(${rotation}deg)`, transformOrigin, - width: bounds.r - bounds.x + this._resizeBorderWidth + 'px', - height: bounds.b - bounds.y + this._resizeBorderWidth + (this._showNothing ? 0 : this._titleHeight) + 'px', + width: bounds.r - bounds.x + 2 * this._resizeBorderWidth + 'px', + height: bounds.b - bounds.y + 2 * this._resizeBorderWidth + (this._showNothing ? 0 : this._titleHeight) + 'px', }}> <div className="documentDecorations-topbar" @@ -818,8 +818,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora display: hideDeleteButton && hideTitle && hideOpenButton ? 'none' : undefined, }} onPointerDown={this.onContainerDown}> - {hideDeleteButton ? null : topBtn('close', 'times', undefined, () => this.onCloseClick(true), 'Close')} - {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, () => this.onCloseClick(undefined), 'Minimize')} + <div className="documentDecorations-closeWrapper"> + {hideDeleteButton ? null : topBtn('close', 'times', undefined, () => this.onCloseClick(true), 'Close')} + {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, () => this.onCloseClick(undefined), 'Minimize')} + </div> {titleArea} {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection, opption: in editor view)')} </div> @@ -832,7 +834,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora <div key="c" className="documentDecorations-centerCont" /> <div key="r" className="documentDecorations-rightResizer" onPointerDown={this.onPointerDown} /> <div key="bl" className="documentDecorations-bottomLeftResizer" onPointerDown={this.onPointerDown} /> - <div key="b" className="documentDecorations-bottomResizer" onPointerDown={this.onPointerDown} /> + {seldocview.TagPanelHeight !== 0 || seldocview.TagPanelEditing ? null : <div key="b" className="documentDecorations-bottomResizer" onPointerDown={this.onPointerDown} />} <div key="br" className="documentDecorations-bottomRightResizer" onPointerDown={this.onPointerDown} /> </> )} @@ -848,12 +850,12 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora /> )} - {hideDocumentButtonBar || this._showNothing ? null : ( + {seldocview.TagPanelEditing || hideDocumentButtonBar || this._showNothing ? null : ( <div className="link-button-container" style={{ - top: DocumentView.Selected().length > 1 ? 0 : `${seldocview.showTags ? 4 + seldocview.TagPanelHeight : 4}px`, - transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `, + top: DocumentView.Selected().length > 1 || !seldocview.showTags ? 0 : `${seldocview.TagPanelHeight}px`, + transform: `translate(${-this._resizeBorderWidth + 10}px, ${(seldocview.TagPanelHeight === 0 ? 2 * this._resizeBorderWidth : this._resizeBorderWidth) + bounds.b - bounds.y + this._titleHeight}px) `, }}> <DocumentButtonBar views={() => DocumentView.Selected()} /> </div> @@ -861,10 +863,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora <div className="documentDecorations-tagsView" style={{ - top: 30, // offset by height of documentButtonBar so that items can be clicked without overlap interference - transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `, + display: DocumentView.Selected().length > 1 || !seldocview.showTags ? 'none' : undefined, + transform: `translate(${-this._resizeBorderWidth + 10}px, ${2 * this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `, }}> - {DocumentView.Selected().length > 1 ? <TagsView Views={DocumentView.Selected()} /> : null} + {DocumentView.Selected().length > 1 ? <TagsView Views={DocumentView.Selected()} background={SnappingManager.userBackgroundColor ?? ''} /> : null} </div> </div> diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index e2490cec8..d9447b7ec 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -287,8 +287,8 @@ export class EditableView extends ObservableReactComponent<EditableProps> { staticDisplay = () => { let toDisplay; - const gval = this._props.GetValue()?.replace(/\n/g, '\\r\\n'); if (this._props.inputString) { + const gval = this._props.GetValue()?.replace(/\n/g, '\\r\\n'); toDisplay = ( <input className="editableView-input" @@ -315,15 +315,17 @@ export class EditableView extends ObservableReactComponent<EditableProps> { }; render() { - const gval = this._props.GetValue()?.replace(/\n/g, '\\r\\n'); - if (this._editing && gval !== undefined) { - return this._props.sizeToContent ? ( - <div style={{ display: 'grid', minWidth: 100 }}> - <div style={{ display: 'inline-block', position: 'relative', height: 0, width: '100%', overflow: 'hidden' }}>{this.renderEditor()}</div> - </div> - ) : ( - this.renderEditor() - ); + if (this._editing) { + const gval = this._props.GetValue()?.replace(/\n/g, '\\r\\n'); + if (gval !== undefined) { + return this._props.sizeToContent ? ( + <div style={{ display: 'grid', minWidth: 100 }}> + <div style={{ display: 'inline-block', position: 'relative', height: 0, width: '100%', overflow: 'hidden' }}>{this.renderEditor()}</div> + </div> + ) : ( + this.renderEditor() + ); + } } setTimeout(() => this._props.autosuggestProps?.resetValue()); return ( diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 37060d20c..94c023330 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -11,14 +11,13 @@ import { GroupManager } from '../util/GroupManager'; import { SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; -import { UndoManager, undoable } from '../util/UndoManager'; +import { UndoManager } from '../util/UndoManager'; import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; import { InkStrokeProperties } from './InkStrokeProperties'; import { MainView } from './MainView'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline'; -import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView } from './nodes/DocumentView'; @@ -242,7 +241,7 @@ export class KeyManager { { const importBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImports); if (importBtn) { - MainView.Instance.selectMenu(importBtn); + MainView.Instance.selectLeftSidebarButton(importBtn); } } break; @@ -250,7 +249,7 @@ export class KeyManager { { const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyTrails); if (trailsBtn) { - MainView.Instance.selectMenu(trailsBtn); + MainView.Instance.selectLeftSidebarButton(trailsBtn); } } break; @@ -260,7 +259,7 @@ export class KeyManager { } else { const searchBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MySearcher); if (searchBtn) { - MainView.Instance.selectMenu(searchBtn); + MainView.Instance.selectLeftSidebarButton(searchBtn); } } break; diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx index f91f98807..1293aa9d0 100644 --- a/src/client/views/InkTranscription.tsx +++ b/src/client/views/InkTranscription.tsx @@ -382,7 +382,7 @@ export class InkTranscription extends React.Component { render() { return ( - <div className="ink-transcription"> + <div className="ink-transcription" style={{pointerEvents: 'none'}}> <div className="math-editor" ref={this.setMathRef}></div> <div className="text-editor" ref={this.setTextRef}></div> </div> diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index e70f9e5ed..be6e2fecb 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -529,6 +529,7 @@ export class MainView extends ObservableReactComponent<object> { fa.faColumns, fa.faChevronCircleUp, fa.faUpload, + fa.faBorderStyle, fa.faBorderAll, fa.faBraille, fa.faPersonChalkboard, @@ -847,7 +848,7 @@ export class MainView extends ObservableReactComponent<object> { } @action - selectMenu = (button: Doc) => { + selectLeftSidebarButton = (button: Doc) => { const title = StrCast(button.$title); const willOpen = !this._leftMenuFlyoutWidth || this._panelContent !== title; this.closeFlyout(); @@ -859,7 +860,9 @@ export class MainView extends ObservableReactComponent<object> { case 'Help': break; default: - this.expandFlyout(button); + this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250; + this._sidebarContent.proto = DocCast(button.target); + SnappingManager.SetLastPressedBtn(button[Id]); } } return true; @@ -935,19 +938,6 @@ export class MainView extends ObservableReactComponent<object> { </div> ); } - - expandFlyout = action((button: Doc) => { - // bcz: What's going on here!? --- may be fixed now, so commenting out ... - // Chrome(not firefox) seems to have a bug when the flyout expands and there's a zoomed freeform tab. All of the div below the CollectionFreeFormView's main div - // generate the wrong value from getClientRectangle() -- specifically they return an 'x' that is the flyout's width greater than it should be. - // interactively adjusting the flyout fixes the problem. So does programmatically changing the value after a timeout to something *fractionally* different (ie, 1.5, not 1);) - this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250; - // setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5))); - - this._sidebarContent.proto = DocCast(button.target); - SnappingManager.SetLastPressedBtn(button[Id]); - }); - closeFlyout = action(() => { SnappingManager.SetLastPressedBtn(''); this._panelContent = 'none'; @@ -1161,7 +1151,7 @@ export class MainView extends ObservableReactComponent<object> { // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function selectMainMenu(doc: Doc) { - MainView.Instance.selectMenu(doc); + MainView.Instance.selectLeftSidebarButton(doc); }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function hideUI() { diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index a1e8fe7ba..6778a6691 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -118,7 +118,7 @@ export class PropertiesButtons extends React.Component { // select text return this.propertyToggleBtn( on => (on ? 'ALIGN TOP' : 'ALIGN CENTER'), - '_layout_centered', + 'text_centered', on => `${on ? 'Text is aligned with top of document' : 'Text is aligned with center of document'} `, () => <MdTouchApp /> // 'eye' ); @@ -293,7 +293,7 @@ export class PropertiesButtons extends React.Component { @computed get onClickVal() { const linkButton = IsFollowLinkScript(this.selectedDoc.onClick); const followLoc = this.selectedDoc._followLinkLocation; - const linkedToLightboxView = () => Doc.Links(this.selectedDoc).some(link => Doc.getOppositeAnchor(link, this.selectedDoc)?._isLightbox); + const linkedToLightboxView = () => Doc.Links(this.selectedDoc).some(link => Doc.getOppositeAnchor(link, this.selectedDoc)?.$isLightbox); if (followLoc === OpenWhere.lightbox && !linkedToLightboxView()) return 'linkInPlace'; if (linkButton && followLoc === OpenWhere.addRight) return 'linkOnRight'; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 7e9cd002b..7fcb15afe 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -13,7 +13,7 @@ import ResizeObserver from 'resize-observer-polyfill'; import { ClientUtils, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; import { Doc, DocListCast, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast, returnEmptyDoclist } from '../../fields/Doc'; -import { AclAdmin, DocAcl, DocData } from '../../fields/DocSymbols'; +import { AclAdmin, DocAcl, DocData, DocLayout } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; @@ -205,7 +205,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps const ids = new Set<string>(reqdKeys); const docs: Doc[] = DocumentView.Selected().length < 2 // - ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] + ? [this.layoutFields ? this.selectedDoc[DocLayout] : this.dataDoc] : DocumentView.Selected().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); docs.forEach(doc => Object.keys(doc) @@ -264,7 +264,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps const docs = DocumentView.Selected().length < 2 && this.selectedDoc ? [this.layoutFields - ? Doc.Layout(this.selectedDoc) // + ? this.selectedDoc[DocLayout] // : this.dataDoc!] : DocumentView.Selected().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); // prettier-ignore docs.forEach(doc => { @@ -330,7 +330,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps return '-- multiple selected --'; } if (this.selectedDoc) { - const layoutDoc = Doc.Layout(this.selectedDoc); + const layoutDoc = this.selectedDoc[DocLayout]; const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes('FormattedTextBox') ? this.rtfHeight : this.docHeight; const panelWidth = StrCast(Doc.LayoutField(layoutDoc)).includes('FormattedTextBox') ? this.rtfWidth : this.docWidth; return ( @@ -894,9 +894,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps }); } @computed get borderColor() { - const doc = this.selectedDoc; - const layoutDoc = doc ? Doc.Layout(doc) : doc; - return StrCast(layoutDoc.color); + return StrCast(this.selectedDoc._color); } set borderColor(value) { this.selectedDoc && (this.selectedDoc.$color = value || undefined); } // prettier-ignore diff --git a/src/client/views/SidebarAnnos.scss b/src/client/views/SidebarAnnos.scss index d7de2b641..abfd04f11 100644 --- a/src/client/views/SidebarAnnos.scss +++ b/src/client/views/SidebarAnnos.scss @@ -1,3 +1,13 @@ +.sidebarAnnos-container { + position: absolute; + width: 100%; + height: 100%; + right: 0; + .sidebarAnnos-stacking { + width: 100%; + position: relative; + } +} .sidebarAnnos-tagList { display: flex; flex-direction: row; diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 3c0611f03..b7e6318b1 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -213,14 +213,11 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr // TODO: Calculation of the topbar is hardcoded and different for text nodes - it should all be the same and all be part of SidebarAnnos return !this._props.showSidebar ? null : ( <div + className="sidebarAnnos-container" style={{ - position: 'absolute', pointerEvents: this._props.isContentActive() ? 'all' : undefined, top: this._props.Doc.type !== DocumentType.RTF && StrCast(this._props.Doc._layout_showTitle) === 'title' ? 15 : 0, - right: 0, background: this._props.styleProvider?.(this._props.Doc, this._props, StyleProp.WidgetColor) as string, - width: `100%`, - height: '100%', }}> <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}> {this.allUsers.length > 1 ? this.allUsers.map(renderUsers) : null} @@ -228,7 +225,7 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr {Array.from(this.allMetadata.keys()).sort().map(renderMeta)} </div> - <div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}> + <div className="sidebarAnnos-stacking" style={{ height: `calc(100% - ${this.filtersHeight()}px)` }}> <CollectionStackingView {...this._props} setContentViewBox={emptyFunction} diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss index ce00f6101..99796f1fb 100644 --- a/src/client/views/StyleProvider.scss +++ b/src/client/views/StyleProvider.scss @@ -1,14 +1,13 @@ .styleProvider-filter, -.styleProvider-audio, .styleProvider-paint, .styleProvider-paint-selected, .styleProvider-lock { z-index: 2; // has to be above title which is z-index 1 font-size: 10; - width: 15; - height: 15; + width: 20; + height: 20; position: absolute; - right: -15; + right: -20; top: 0; background: black; pointer-events: all; @@ -21,25 +20,25 @@ cursor: default; } .styleProvider-filter { - right: 15; + right: 20; .styleProvider-filterShift { left: 0; top: 0; position: absolute; } -} -.styleProvider-audio { - right: 30; + .dropdown-container { + width: 15px !important; + margin: auto !important; + } } .styleProvider-paint-selected, .styleProvider-paint { top: 15; } .styleProvider-paint-selected { - right: -30; + right: -40; } .styleProvider-lock:hover, -.styleProvider-audio:hover, .styleProvider-filter:hover { opacity: 1; } diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 331ee1707..e8876f96f 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -1,7 +1,6 @@ import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from '@dash/components'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@mui/material'; import { action, untracked } from 'mobx'; import { extname } from 'path'; import * as React from 'react'; @@ -9,18 +8,19 @@ import { BsArrowDown, BsArrowDownUp, BsArrowUp } from 'react-icons/bs'; import { FaFilter } from 'react-icons/fa'; import { ClientUtils, DashColor, lightOrDark } from '../../ClientUtils'; import { Doc, Opt, StrListCast } from '../../fields/Doc'; +import { DocLayout } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { InkInkTool } from '../../fields/InkField'; import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types'; -import { AudioAnnoState } from '../../server/SharedMediaTypes'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { IsFollowLinkScript } from '../documents/DocUtils'; import { SnappingManager } from '../util/SnappingManager'; import { undoable, UndoManager } from '../util/UndoManager'; import { TreeSort } from './collections/TreeSort'; import { Colors } from './global/globalEnums'; -import { DocumentView, DocumentViewProps } from './nodes/DocumentView'; +import { DocumentViewProps } from './nodes/DocumentContentsView'; +import { DocumentView } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; import { StyleProp } from './StyleProp'; import './StyleProvider.scss'; @@ -87,7 +87,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & const isNonTransparent = property.includes(':nonTransparent'); const isNonTransparentLevel = isNonTransparent ? Number(property.replace(/.*:nonTransparent([0-9]+).*/, '$1')) : 0; // property.includes(':nonTransparent'); const isAnnotated = property.includes(':annotated'); - const layoutDoc = doc ? Doc.Layout(doc) : doc; + const layoutDoc = doc?.[DocLayout]; const isOpen = property.includes(':treeOpen'); const boxBackground = property.includes(':docView'); // background color of docView's bounds can be different than the background of contents -- eg FontIconBox const { @@ -108,8 +108,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & LayoutTemplateString, disableBrushing, NativeDimScaling, - PanelWidth, - PanelHeight, isSelected, isHovering, } = props || {}; // extract props that are not shared between fieldView and documentView props. @@ -250,16 +248,23 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & : 0; case StyleProp.BackgroundColor: { if (SnappingManager.LastPressedBtn === doc?.[Id]) return SnappingManager.userColor; // hack to indicate active menu panel item - const dataKey = doc ? Doc.LayoutFieldKey(doc) : ""; - const usePath = StrCast(doc?.[dataKey + "_usePath"]); - const alternate = usePath.includes(":hover") ? ( isHovering?.() ? '_' + usePath.replace(":hover","") : "") : usePath ? "_" +usePath:usePath; - let docColor: Opt<string> = StrCast(doc?.[fieldKey+alternate], StrCast(doc?.['backgroundColor' +alternate], isCaption ? 'rgba(0,0,0,0.4)' : '')); - if (!docColor && doc?.[StrCast(doc?.layout_fieldKey)] instanceof Doc) docColor = StrCast(doc._backgroundColor,docColor) + const dataKey = doc ? Doc.LayoutFieldKey(doc) : ''; + const usePath = StrCast(doc?.[dataKey + '_usePath']); + const alternate = usePath.includes(':hover') ? ( isHovering?.() ? '_' + usePath.replace(':hover','') : '') : usePath ? "_" +usePath:usePath; + let docColor:Opt<string> = layoutDoc && + StrCast(alternate ? layoutDoc['backgroundColor' + alternate]:undefined, + doc.rootDocument + ? StrCast(layoutDoc.backgroundColor, StrCast(DocCast(doc.rootDocument).backgroundColor)) // for nested templates: use template's color, then root doc's color + : layoutDoc === doc + ? StrCast(doc.backgroundColor) + : StrCast(StrCast(Doc.GetT(layoutDoc, 'backgroundColor', 'string', true), StrCast(doc.backgroundColor, StrCast(layoutDoc.backgroundColor)) // otherwise, use expanded template coloor, then root doc's color, then template's inherited color + ))); + // prettier-ignore switch (layoutDoc?.type) { 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.FONTICON: docColor = boxBackground ? undefined : docColor || SnappingManager.userBackgroundColor; 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; @@ -322,7 +327,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')}` - :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) + : 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, ''); } @@ -337,25 +342,28 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & if (isDocumentActive?.()) return isInk() ? 'visiblePainted' : 'all'; return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active case StyleProp.Decorations: { - const lock = () => doc?.pointerEvents !== 'none' ? null : ( + const showLock = doc?.pointerEvents === 'none' + const lock = () => !showLock ? null : ( <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}> <FontAwesomeIcon icon='lock' size="lg" /> </div> ); - const paint = () => !doc?.onPaint ? null : ( + const showPaint = doc?.onPaint; + const paint = () => !showPaint ? null : ( <div className={`styleProvider-paint${isSelected?.() ? "-selected":""}`} onClick={e => togglePaintView(e, doc, props)}> <FontAwesomeIcon icon='pen' size="lg" /> </div> ); + const showFilterIcon = + StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length + ? 'green' // #18c718bd' //'hasFilter' + : childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || childFiltersByRanges?.().length + ? 'orange' // 'inheritsFilter' + : undefined; + const showFilter = showFilterIcon && !hideFilterStatus; const filter = () => { const dashView = untracked(() => DocumentView.getDocumentView(Doc.ActiveDashboard)); - const showFilterIcon = - StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length - ? 'green' // #18c718bd' //'hasFilter' - : childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || childFiltersByRanges?.().length - ? 'orange' // 'inheritsFilter' - : undefined; - return !showFilterIcon || hideFilterStatus ? null : ( + return !showFilter ? null : ( <div className="styleProvider-filter"> <Dropdown type={Type.TERT} @@ -387,26 +395,13 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & </div> ); }; - const audio = () => { - const audioAnnoState = (audioDoc: Doc) => StrCast(audioDoc.audioAnnoState, AudioAnnoState.stopped); - const audioAnnosCount = (audioDoc: Doc) => StrListCast(audioDoc[fieldKey + 'audioAnnotations']).length; - if (!doc || renderDepth === -1 || !audioAnnosCount(doc)) return null; - const audioIconColors: { [key: string]: string } = { playing: 'green', stopped: 'blue' }; - return ( - <Tooltip title={<div>{StrListCast(doc[fieldKey + 'audioAnnotations_text']).lastElement()}</div>}> - <div className="styleProvider-audio" onPointerDown={() => DocumentView.getFirstDocumentView(doc)?.playAnnotation()}> - <FontAwesomeIcon className="documentView-audioFont" style={{ color: audioIconColors[audioAnnoState(doc)] }} icon='file-audio' size="sm" /> - </div> - </Tooltip> - ); - }; return ( + !showPaint && !showLock && !showFilter ? null: <> {paint()} {lock()} {filter()} - {audio()} </> ); } diff --git a/src/client/views/TagsView.scss b/src/client/views/TagsView.scss index b21d303fb..a940502d4 100644 --- a/src/client/views/TagsView.scss +++ b/src/client/views/TagsView.scss @@ -2,21 +2,23 @@ display: flex; flex-wrap: wrap; flex-direction: column; - border: 1px solid; - border-radius: 4px; width: 100%; position: relative; .tagsView-content { width: 100%; height: inherit; + position: absolute; .tagsView-list { display: flex; flex-wrap: wrap; - height: 1; + height: 100%; .iconButton-container { min-height: unset !important; } } + .tagsView-editing-box { + pointer-events: all; + } } } @@ -57,10 +59,6 @@ align-items: center; } -.tagsView-editing-box { - margin-top: 20px; -} - .tagsView-input-box { margin: auto; align-self: center; diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index 7d2a6be5e..79c4d8af1 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, Colors, IconButton } from '@dash/components'; +import { Button, Colors, IconButton, Type } from '@dash/components'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; @@ -20,6 +20,7 @@ import { DocumentView } from './nodes/DocumentView'; import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; import { IconTagBox } from './nodes/IconTagBox'; import { Id } from '../../fields/FieldSymbols'; +import { StyleProp } from './StyleProp'; /** * The TagsView is a metadata input/display panel shown at the bottom of a DocumentView in a freeform collection. @@ -254,6 +255,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { interface TagViewProps { Views: DocumentView[]; + background: string; } /** @@ -265,11 +267,10 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { super(props); makeObservable(this); } - InsetDist = 25; // how far tag panel is moved up to overlap DocumentView. @observable _panelHeightDirty = 0; @observable _currentInput = ''; - @observable _isEditing = !StrListCast(this.View.dataDoc.tags).length; + @observable _isEditing: boolean | undefined = undefined; _heightDisposer: IReactionDisposer | undefined; _lastXf = this.View.screenToContentsTransform(); @@ -295,7 +296,9 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { } @computed get isEditing() { - return this._isEditing && (this._props.Views.length > 1 || (DocumentView.Selected().length === 1 && DocumentView.Selected().includes(this.View))); + const selected = DocumentView.Selected().length === 1 && DocumentView.Selected().includes(this.View); + if (this._isEditing === undefined) return selected && this.View.TagPanelEditing; // && !StrListCast(this.View.dataDoc.tags).length && !StrListCast(this.View.dataDoc[Doc.LayoutFieldKey(this.View.Document) + '_audioAnnotations_text']).length; + return this._isEditing && (this._props.Views.length > 1 || (selected && this.View.TagPanelEditing)); } /** @@ -305,7 +308,10 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { @action setToEditing = (editing = true) => { this._isEditing = editing; - editing && this._props.Views.length === 1 && this.View.select(false); + if (this._props.Views.length === 1) { + this.View.TagPanelEditing = editing; + editing && this.View.select(false); + } }; /** @@ -346,10 +352,19 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { return this.View.ComponentView?.isUnstyledView?.() || (!this.View.showTags && this._props.Views.length === 1) ? null : ( <div className="tagsView-container" - ref={r => r && new ResizeObserver(action(() => this._props.Views.length === 1 && (this.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)} + ref={r => + r && + new ResizeObserver( + action(() => { + if (this._props.Views.length === 1) { + this.View.TagPanelHeight = Math.floor(r?.children[0].children[0].getBoundingClientRect().height ?? 0) - Math.floor(r?.children[0].children[0].children[0].getBoundingClientRect().height ?? 0); + } + }) + ).observe(r?.children[0]) + } style={{ display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined, - backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, + backgroundColor: this.isEditing ? this._props.background : Colors.TRANSPARENT, borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT, height: !this._props.Views.lastElement()?.isSelected() ? 0 : undefined, }}> @@ -357,18 +372,21 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { <div className="tagsView-list"> {this._props.Views.length === 1 && !this.View.showTags ? null : ( // <IconButton - style={{ width: '8px' }} + style={{ width: '8px', height: this._props.Views.lastElement().TagBtnHeight }} tooltip="Close Menu" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, upEv => { - this.setToEditing(!this._isEditing); + this.setToEditing(!this.isEditing); upEv.stopPropagation(); }) } - icon={<FontAwesomeIcon icon={this._isEditing ? 'chevron-up' : 'chevron-down'} size="sm" />} + type={Type.TERT} + background="transparent" + color={this.View._props.styleProvider?.(this.View.Document, this.View.ComponentView?._props, StyleProp.FontColor) as string} + icon={<FontAwesomeIcon icon={this.isEditing ? 'chevron-up' : 'chevron-down'} size="sm" />} /> )} - <IconTagBox Views={this._props.Views} IsEditing={this._isEditing} /> + <IconTagBox Views={this._props.Views} IsEditing={this.isEditing} /> {Array.from(tagsList) .filter(tag => (tag.startsWith('#') || tag.startsWith('@')) && !Doc.MyFilterHotKeys.some(key => key.toolType === tag)) .map(tag => ( diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index 756b37f99..50de7c601 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -23,7 +23,8 @@ import { undoable, UndoManager } from '../../util/UndoManager'; import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; import { TagItem } from '../TagsView'; -import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import { DocumentViewProps } from '../nodes/DocumentContentsView'; +import { DocumentView } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import './CollectionCardDeckView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index 544b3e262..962b590c8 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -1,12 +1,16 @@ .collectionCarouselView-outer { height: 100%; - position: relative; + position: absolute; overflow: hidden; display: flex; + transform-origin: top left; + .collectionCarouselView-caption { height: 50; display: inline-block; width: 100%; + bottom: 0; + position: absolute; } .collectionCarouselView-image { height: calc(100% - 50px); diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index a7d217076..2d3f8cb0e 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -115,12 +115,14 @@ export class CollectionCarouselView extends CollectionSubView() { ? false : undefined; // prettier-ignore onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); + renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { return ( <DocumentView {...this._props} ref={overlayFunc} Document={doc} + TemplateDataDocument={doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined} NativeWidth={returnZero} NativeHeight={returnZero} fitWidth={this._props.childLayoutFitWidth} @@ -136,7 +138,6 @@ export class CollectionCarouselView extends CollectionSubView() { renderDepth={this._props.renderDepth + 1} LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} - TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)} childFilters={this.childDocFilters} focus={this.focus} hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)} @@ -182,7 +183,15 @@ export class CollectionCarouselView extends CollectionSubView() { } @computed get content() { - const captionProps = { ...this._props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined }; + const captionProps = { + ...this._props, // + NativeScaling: returnOne, + PanelWidth: this.captionWidth, + fieldKey: 'caption', + setHeight: undefined, + setContentView: undefined, + noSidebar: true, + }; const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption); return !this.curDoc() ? null : ( <> @@ -242,11 +251,9 @@ export class CollectionCarouselView extends CollectionSubView() { 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.navButtons} diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index c79610595..1576a8e4a 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -23,6 +23,7 @@ import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; import './CollectionMenu.scss'; import { CollectionLinearView } from './collectionLinear'; +import { SettingsManager } from '../../util/SettingsManager'; interface CollectionMenuProps { panelHeight: () => number; @@ -118,7 +119,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { <div className="hardCodedButtons"> <Toggle toggleType={ToggleType.BUTTON} - type={Type.PRIM} + type={Type.TERT} color={SnappingManager.userColor} onClick={this.props.toggleTopBar} toggleStatus={this.props.topBarHeight() > 0} @@ -127,7 +128,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { /> <Toggle toggleType={ToggleType.BUTTON} - type={Type.PRIM} + type={Type.TERT} color={SnappingManager.userColor} onClick={this._props.togglePropertiesFlyout} toggleStatus={SnappingManager.PropertiesWidth > 0} @@ -138,19 +139,17 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { ); // dash col linear view buttons - const contMenuButtons = ( + return ( <div className="collectionMenu-container" style={{ - background: SnappingManager.userBackgroundColor, + background: SettingsManager.userBackgroundColor, // borderColor: SettingsManager.userColor }}> {this.contMenuButtons} {hardCodedButtons} </div> ); - - return contMenuButtons; } } diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 01695dbaf..173147f64 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -258,11 +258,9 @@ export class CollectionNoteTakingView extends CollectionSubView() { const noteTakingDocTransform = () => this.getDocTransform(doc, dref); return ( <DocumentView - ref={r => { - dref = r || undefined; - }} + ref={r => (dref = r || undefined)} Document={doc} - TemplateDataDocument={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)} + TemplateDataDocument={doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined} pointerEvents={this.blockPointerEventsWhenDragging} renderDepth={this._props.renderDepth + 1} PanelWidth={width} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index fd48a9dc1..4f60acb18 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -112,7 +112,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection @computed get columnWidth() { const availableWidth = this._props.PanelWidth() - 2 * this.xMargin; const cwid = availableWidth / (NumCast(this.Document._layout_columnCount) || this._props.PanelWidth() / NumCast(this.Document._layout_columnWidth, this._props.PanelWidth() / 4)); - return Math.min(availableWidth, this.isStackingView ? Number.MAX_VALUE : cwid - this.gridGap); + return Math.min(availableWidth, this.isStackingView ? availableWidth / (this.numGroupColumns || 1) : cwid - this.gridGap); } @computed get NodeWidth() { @@ -675,12 +675,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } return35 = () => 35; + @computed get menuBtnDoc() { return DocCast(this.layoutDoc.layout_headerButton); } // prettier-ignore @computed get buttonMenu() { - const menuDoc = DocCast(this.layoutDoc.layout_headerButton); - return !menuDoc ? null : ( - <div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}> + return !this.menuBtnDoc ? null : ( + <div className="buttonMenu-docBtn" style={{ width: NumCast(this.menuBtnDoc._width, 30), height: NumCast(this.menuBtnDoc._height, 30) }}> <DocumentView - Document={menuDoc} + Document={this.menuBtnDoc} isContentActive={this.isContentActive} isDocumentActive={this.isContentActive} addDocument={this._props.addDocument} @@ -728,13 +728,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection SetValue: this.addGroup, contents: '+ ADD A GROUP', }; - const buttonMenu = this.layoutDoc.layout_headerButton; const noviceExplainer = this.layoutDoc.layout_explainer; return ( <> - {buttonMenu || noviceExplainer ? ( + {this.menuBtnDoc || noviceExplainer ? ( <div className="documentButtonMenu"> - {buttonMenu ? this.buttonMenu : null} + {this.menuBtnDoc ? this.buttonMenu : null} {Doc.noviceMode && noviceExplainer ? <div className="documentExplanation">{StrCast(noviceExplainer)}</div> : null} </div> ) : null} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 3b9d167c6..66839ba7f 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -7,7 +7,7 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { RichTextField } from '../../../fields/RichTextField'; import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, NumCast } from '../../../fields/Types'; +import { BoolCast, DocCast, NumCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; @@ -26,6 +26,7 @@ import { EditableView } from '../EditableView'; import { DocumentView } from '../nodes/DocumentView'; import { ObservableReactComponent } from '../ObservableReactComponent'; import './CollectionStackingView.scss'; +import { DocData } from '../../../fields/DocSymbols'; // So this is how we are storing a column interface CSVFieldColumnProps { @@ -270,7 +271,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< event: () => { const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); if (created) { - const container = this._props.Doc.resolvedDataDoc ? Doc.GetProto(this._props.Doc) : this._props.Doc; + const container = DocCast(this._props.Doc.rootDocument)?.[DocData] ? Doc.GetProto(this._props.Doc) : this._props.Doc; if (container.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(created, container); return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created); @@ -304,10 +305,8 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< @computed get innards() { TraceMobx(); const key = this._props.pivotField; - const headings = this._props.headings(); const heading = this._heading; const columnYMargin = this._props.headingObject ? 0 : this._props.yMargin; - const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); const noValueHeader = `NO ${key.toUpperCase()} VALUE`; const evContents = heading || (this._props?.type === 'number' ? '0' : noValueHeader); const headingView = this._props.headingObject ? ( @@ -317,7 +316,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< ref={this._headerRef} style={{ marginTop: this._props.yMargin, - width: this._props.columnWidth / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1), + width: this._props.columnWidth }}> {/* the default bucket (no key value) has a tooltip that describes what it is. Further, it does not have a color and cannot be deleted. */} @@ -360,7 +359,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< margin: 'auto', marginTop: this._props.dontCenter.includes('y') ? undefined : 'auto', marginBottom: this._props.dontCenter.includes('y') ? undefined : 'auto', - width: this._props.columnWidth / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1), + width: this._props.columnWidth, }}> <div key={`${heading}-stack`} @@ -368,7 +367,6 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< style={{ padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`, margin: this._props.dontCenter.includes('x') ? undefined : 'auto', - // width: 'max-content', // singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, height: 'max-content', position: 'relative', gridGap: this._props.gridGap, diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a43cf0755..375c0fe53 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -13,6 +13,7 @@ import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from ' import { WebField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; +import { Upload } from '../../../server/SharedMediaTypes'; import { DocServer } from '../../DocServer'; import { Networking } from '../../Network'; import { DocUtils } from '../../documents/DocUtils'; @@ -24,11 +25,11 @@ import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { SnappingManager } from '../../util/SnappingManager'; import { UndoManager } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; +import { DocumentViewProps } from '../nodes/DocumentContentsView'; +import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; -import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; -import { FlashcardPracticeUI } from './FlashcardPracticeUI'; import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere'; -import { Upload } from '../../../server/SharedMediaTypes'; +import { FlashcardPracticeUI } from './FlashcardPracticeUI'; export enum docSortings { Time = 'time', @@ -108,11 +109,11 @@ export function CollectionSubView<X>() { } get dataDoc() { - return this._props.TemplateDataDocument instanceof Doc && this.layoutDoc.isTemplateForField // - ? this._props.TemplateDataDocument[DocData] - : this.layoutDoc.resolvedDataDoc - ? this._props.Document - : this.Document[DocData]; // if the layout document has a resolvedDataDoc, then we don't want to get its parent which would be the unexpanded template + return this._props.TemplateDataDocument instanceof Doc && this.Document.isTemplateForField // + ? Doc.GetProto(this._props.TemplateDataDocument) + : this.Document.rootDocument + ? this.Document + : this.Document[DocData]; // if the layout document has a rootDocument, then we don't want to get its parent which would be the unexpanded template } get childContainerViewPath() { @@ -131,9 +132,9 @@ export function CollectionSubView<X>() { hasChildDocs = () => this.childLayoutPairs.map(pair => pair.layout); @computed get childLayoutPairs(): { layout: Doc; data: Doc }[] { - const { Document: Document, TemplateDataDocument } = this._props; + const { TemplateDataDocument } = this._props; const validPairs = this.childDocs - .map(doc => Doc.GetLayoutDataDocPair(Document, !this._props.isAnnotationOverlay ? TemplateDataDocument : undefined, doc)) + .map(doc => Doc.GetLayoutDataDocPair(this.Document, !this._props.isAnnotationOverlay ? TemplateDataDocument : undefined, doc)) .filter( pair => // filter out any documents that have a proto that we don't have permissions to @@ -299,7 +300,7 @@ export function CollectionSubView<X>() { const dragData = de.complete.docDragData; if (dragData) { const sourceDragAction = dragData.dropAction; - const sameCollection = !dragData.draggedDocuments.some(d => d.embedContainer !== this._renderDoc); + const sameCollection = !dragData.draggedDocuments.some(d => d.embedContainer !== this.Document); dragData.dropAction = !sameCollection // if doc from another tree ? sourceDragAction || targetDropAction // then use the source's dragAction otherwise the target's : sourceDragAction === dropActionType.inPlace // if source drag is inPlace @@ -589,7 +590,7 @@ export function CollectionSubView<X>() { /** * 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; } // prettier-ignore + @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (!this._props.fitWidth?.(this.Document) ? this._props.NativeDimScaling?.()||1: 1); } // 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 @@ -602,7 +603,12 @@ export function CollectionSubView<X>() { */ @computed get uiBtnScaling() { return this.maxWidgetSize / this._sideBtnWidth; } // prettier-ignore - screenXPadding = () => (this.uiBtnScaling * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale; + screenXPadding = (docView?: DocumentView) => { + if (!docView) return 0; + const diff = this._props.PanelWidth() - docView.PanelWidth(); + const xpad1 = this.uiBtnScaling * (this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) - diff / 2; // this._sideBtnWidth; + return xpad1 / (docView.NativeDimScaling?.() || 1); + }; filteredChildDocs = () => this.childLayoutPairs.map(pair => pair.layout); childDocsFunc = () => this.childDocs; @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 1960e12bd..962fbdcd7 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -8,7 +8,7 @@ import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; @@ -187,7 +187,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @action addDoc = (docs: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => { const addDocRelativeTo = (adocs: Doc | Doc[]) => (adocs as Doc[]).reduce((flg, doc) => flg && Doc.AddDocToList(this.Document[DocData], this._props.fieldKey, doc, relativeTo, before), true); - if (this.Document.resolvedDataDoc instanceof Promise) return false; + if (this.Document.rootDocument instanceof Promise) return false; const doclist = toList(docs); const res = relativeTo === undefined ? this._props.addDocument?.(doclist) || false : addDocRelativeTo(doclist); res && @@ -200,7 +200,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree onContextMenu = (): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout const layoutItems: ContextMenuProps[] = []; - const menuDoc = ScriptCast(Cast(this.layoutDoc.layout_headerButton, Doc, null)?.onClick)?.script.originalScript === CollectionTreeView.AddTreeFunc; + const menuDoc = ScriptCast(this.menuBtnDoc?.onClick)?.script.originalScript === CollectionTreeView.AddTreeFunc; menuDoc && layoutItems.push({ description: 'Create new folder', event: () => CollectionTreeView.addTreeFolder(this.Document), icon: 'paint-brush' }); if (!Doc.noviceMode) { layoutItems.push({ @@ -347,14 +347,14 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree } return35 = () => 35; + @computed get menuBtnDoc() { return DocCast(this.layoutDoc.layout_headerButton); } // prettier-ignore @computed get buttonMenu() { - const menuDoc = Cast(this.layoutDoc.layout_headerButton, Doc, null); // To create a multibutton menu add a CollectionLinearView - return !menuDoc ? null : ( - <div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}> + return !this.menuBtnDoc ? null : ( + <div className="buttonMenu-docBtn" style={{ width: NumCast(this.menuBtnDoc._width, 30), height: NumCast(this.menuBtnDoc._height, 30) }}> <DocumentView - Document={menuDoc} - TemplateDataDocument={menuDoc} + Document={this.menuBtnDoc} + TemplateDataDocument={this.menuBtnDoc} isContentActive={this._props.isContentActive} isDocumentActive={returnTrue} addDocument={this._props.addDocument} diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 7ba8989ce..c9af92a1b 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -130,7 +130,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr { description: 'Calendar', event: () => func(CollectionViewType.Calendar), icon: 'columns' }, { description: 'Pivot', event: () => func(CollectionViewType.Pivot), icon: 'columns' }, { description: 'Time', event: () => func(CollectionViewType.Time), icon: 'columns' }, - { description: 'Map', event: () => func(CollectionViewType.Map), icon: 'globe-americas' }, { description: 'Grid', event: () => func(CollectionViewType.Grid), icon: 'th-list' }, ]; @@ -162,7 +161,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr if (this.Document.childClickedOpenTemplateView instanceof Doc) { optionItems.push({ description: 'View Child Detailed Layout', event: () => this._props.addDocTab(this.Document.childClickedOpenTemplateView as Doc, OpenWhere.addRight), icon: 'project-diagram' }); } - !Doc.noviceMode && optionItems.push({ description: `${this.layoutDoc._isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => { this.layoutDoc._isLightbox = !this.layoutDoc._isLightbox; }, icon: 'project-diagram' }); // prettier-ignore + !Doc.noviceMode && optionItems.push({ description: `${this.dataDoc.$isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => { this.dataDoc.$isLightbox = !this.dataDoc.$isLightbox; }, icon: 'project-diagram' }); // prettier-ignore !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'hand-point-right' }); diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx index c071c5fb8..f24a8acb7 100644 --- a/src/client/views/collections/FlashcardPracticeUI.tsx +++ b/src/client/views/collections/FlashcardPracticeUI.tsx @@ -7,13 +7,16 @@ 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 { Doc, DocListCast, Opt } 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 { DocumentView } from '../nodes/DocumentView'; import './FlashcardPracticeUI.scss'; +import { StyleProp } from '../StyleProp'; +import { FieldViewProps } from '../nodes/FieldView'; +import { DocumentViewProps } from '../nodes/DocumentContentsView'; export enum practiceMode { PRACTICE = 'practice', @@ -60,8 +63,8 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp @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_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)); + btnHeight = () => NumCast(this.filterDoc?.height); + btnWidth = () => (!this.filterDoc ? 1 : NumCast(this.filterDoc._width)); /** * Sets the practice mode answer style for flashcards @@ -131,7 +134,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp <div className="FlashcardPracticeUI-practiceModes" style={{ - transform: this._props.ScreenToLocalBoxXf().Scale >= 1 ? undefined : `translateY(${this.btnHeight() / this._props.ScreenToLocalBoxXf().Scale - this.btnHeight()}px)`, + background: SnappingManager.userVariantColor, }}> <MultiToggle tooltip="Practice flashcards one at a time" @@ -139,7 +142,6 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp color={SnappingManager.userColor} background={SnappingManager.userVariantColor} multiSelect={false} - isToggle={false} toggleStatus={!!this.practiceMode} label="Practice" items={[ @@ -159,7 +161,6 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp color={SnappingManager.userColor} background={SnappingManager.userVariantColor} multiSelect={false} - isToggle={false} toggleStatus={!!this.practiceMode} label={StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)} items={[ @@ -179,6 +180,12 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp </div> ); } + childStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => { + if (doc instanceof Doc && property === StyleProp.BackgroundColor) { + return SnappingManager.userVariantColor; + } + return this._props.docViewProps().styleProvider?.(doc, props, property); + }; tryFilterOut = (doc: Doc) => (this.practiceMode && doc?._layout_flashcardType && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct render() { return ( @@ -186,24 +193,27 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp {this.emptyMessage} {this.practiceButtons} {this._props.layoutDoc._chromeHidden ? null : ( - <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaling})` }}> + <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnWidth(), transform: `scale(${this._props.uiBtnScaling})` }}> {!this.filterDoc ? null : ( - <DocumentView - {...this._props.docViewProps()} - Document={this.filterDoc} - TemplateDataDocument={undefined} - PanelWidth={this.btnWidth} - PanelHeight={this.btnHeight} - NativeWidth={returnZero} - NativeHeight={returnZero} - hideDecorations={BoolCast(this._props.layoutDoc.layout_hideDecorations)} - hideCaptions={true} - hideFilterStatus={true} - renderDepth={this._props.renderDepth + 1} - fitWidth={undefined} - showTags={false} - setContentViewBox={undefined} - /> + <div style={{ background: SnappingManager.userVariantColor }}> + <DocumentView + {...this._props.docViewProps()} + Document={this.filterDoc} + TemplateDataDocument={undefined} + PanelWidth={this.btnWidth} + PanelHeight={this.btnHeight} + NativeWidth={returnZero} + NativeHeight={returnZero} + hideDecorations={BoolCast(this._props.layoutDoc.layout_hideDecorations)} + hideCaptions={true} + hideFilterStatus={true} + renderDepth={this._props.renderDepth + 1} + styleProvider={this.childStyleProvider} + fitWidth={undefined} + showTags={false} + setContentViewBox={undefined} + /> + </div> )} {this.practiceModesMenu} </div> diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss index 397e35ca9..931cdac2b 100644 --- a/src/client/views/collections/TabDocView.scss +++ b/src/client/views/collections/TabDocView.scss @@ -4,7 +4,6 @@ height: 100%; width: 100%; } - input.lm_title:focus, input.lm_title { max-width: unset !important; @@ -22,7 +21,6 @@ input.lm_title { .lm_iconWrap { display: flex; color: black; - width: 15px; height: 15px; align-items: center; align-self: center; @@ -30,6 +28,16 @@ input.lm_title { margin: 3px; border-radius: 20%; + width: auto; + svg:nth-of-type(2) { + display: none; + } + &:hover { + svg:nth-of-type(2) { + display: block; + } + } + .moreInfoDot { background-color: white; border-radius: 100%; diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 620be2726..568a08792 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -10,7 +10,7 @@ import ResizeObserver from 'resize-observer-polyfill'; import { ClientUtils, DashColor, lightOrDark, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { Doc, Opt, returnEmptyDoclist } from '../../../fields/Doc'; -import { DocData } from '../../../fields/DocSymbols'; +import { DocData, DocLayout } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { FieldId } from '../../../fields/RefField'; @@ -41,6 +41,7 @@ import { CollectionDockingView } from './CollectionDockingView'; import { CollectionView } from './CollectionView'; import './TabDocView.scss'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { Tooltip } from '@mui/material'; interface TabMinimapViewProps { doc: Doc; @@ -311,7 +312,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { @observable _view: DocumentView | undefined = undefined; @observable _forceInvalidateScreenToLocal = 0; // screentolocal is computed outside of react using a dom resize ovbserver. this hack allows the resize observer to trigger a react update - @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } // prettier-ignore + @computed get layoutDoc() { return this._document?.[DocLayout]; } // prettier-ignore @computed get isUserActivated() { return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive; } // prettier-ignore @computed get isContentActive() { return this.isUserActivated || this._hovering; } // prettier-ignore @@ -353,7 +354,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { if (tab.element[0].children[1].children.length === 1) { iconWrap.className = 'lm_iconWrap lm_moreInfo'; - iconWrap.title = 'click for menu, drag to embed in document'; const dragBtnDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, @@ -377,7 +377,26 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { ); }; - const docIcon = <FontAwesomeIcon onPointerDown={dragBtnDown} icon={iconType} />; + const docIcon = ( + <> + <Tooltip title="click for menu, drag to embed in document"> + <FontAwesomeIcon onPointerDown={dragBtnDown} icon={iconType} /> + </Tooltip> + <Tooltip title="click to open in lightbox"> + <FontAwesomeIcon + onPointerDown={dragBtnDown} + icon="external-link-alt" + onClick={() => { + if (doc.layout_fieldKey === 'layout_icon') { + const odoc = Doc.GetEmbeddings(doc).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(doc); + Doc.deiconifyView(odoc); + } + this.addDocTab(doc, OpenWhere.lightboxAlways); + }} + /> + </Tooltip> + </> + ); const closeIcon = <FontAwesomeIcon icon="eye" />; ReactDOM.createRoot(iconWrap).render(docIcon); ReactDOM.createRoot(closeWrap).render(closeIcon); diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index cb7da8c84..4dc937864 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { ClientUtils, lightOrDark, return18, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { Doc, DocListCast, Field, FieldType, Opt, StrListCast, returnEmptyDoclist } from '../../../fields/Doc'; -import { DocData } from '../../../fields/DocSymbols'; +import { DocData, DocLayout } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; @@ -156,7 +156,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { return this.Document[DocData]; } @computed get layoutDoc() { - return Doc.Layout(this.Document); + return this.Document[DocLayout]; } @computed get fieldKey() { return StrCast(this.Document._treeView_FieldKey, Doc.LayoutFieldKey(this.Document)); @@ -594,7 +594,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { const key = match[1]; const assign = match[2]; const val = match[3]; - Doc.SetField(doc, key, assign + val, false); + Doc.SetField(doc, key, assign + val); return true; } return false; @@ -1309,7 +1309,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeViewRefs.get(docs[i - 1])); const outdent = !parentCollectionDoc ? undefined : (editTitle: boolean) => dentDoc(editTitle, parentCollectionDoc, containerPrevSibling, parentTreeView instanceof TreeView ? parentTreeView._props.parentTreeView : undefined); const addDocument = (doc: Doc | Doc[], annotationKey?: string, relativeTo?: Doc, before?: boolean) => add(doc, relativeTo ?? docs[i], before !== undefined ? before : false); - const childLayout = Doc.Layout(pair.layout); + const childLayout = pair.layout[DocLayout]; const rowHeight = () => { const aspect = Doc.NativeAspect(childLayout); return aspect ? Math.min(NumCast(childLayout._width), rowWidth()) / aspect : NumCast(childLayout._height); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts index 3838852dd..903d92c90 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts @@ -31,16 +31,14 @@ export class CollectionFreeFormClusters { get selectDocuments() { return this._view.selectDocuments; } // prettier-ignore static overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) { - const doc2Layout = Doc.Layout(doc2); - const doc1Layout = Doc.Layout(doc1); const x2 = NumCast(doc2.x) - clusterDistance; const y2 = NumCast(doc2.y) - clusterDistance; - const w2 = NumCast(doc2Layout._width) + clusterDistance; - const h2 = NumCast(doc2Layout._height) + clusterDistance; + const w2 = NumCast(doc2._width) + clusterDistance; + const h2 = NumCast(doc2._height) + clusterDistance; const x = NumCast(doc1.x) - clusterDistance; const y = NumCast(doc1.y) - clusterDistance; - const w = NumCast(doc1Layout._width) + clusterDistance; - const h = NumCast(doc1Layout._height) + clusterDistance; + const w = NumCast(doc1._width) + clusterDistance; + const h = NumCast(doc1._height) + clusterDistance; return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 }); } handlePointerDown(probe: number[]) { @@ -49,12 +47,11 @@ export class CollectionFreeFormClusters { .reduce((cluster, cd) => { const grouping = this.Document._freeform_useClusters ? NumCast(cd.layout_cluster, -1) : NumCast(cd.group, -1); if (grouping !== -1) { - const layoutDoc = Doc.Layout(cd); const cx = NumCast(cd.x) - this._clusterDistance / 2; const cy = NumCast(cd.y) - this._clusterDistance / 2; - const cw = NumCast(layoutDoc._width) + this._clusterDistance; - const ch = NumCast(layoutDoc._height) + this._clusterDistance; - return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? grouping : cluster; + const cw = NumCast(cd._width) + this._clusterDistance; + const ch = NumCast(cd._height) + this._clusterDistance; + return !cd.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? grouping : cluster; } return cluster; }, -1); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 4ea1de680..158bac7ba 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -1,5 +1,6 @@ /* eslint-disable no-use-before-define */ import { Doc, Field, FieldType, FieldResult } from '../../../../fields/Doc'; +import { DocLayout } from '../../../../fields/DocSymbols'; import { Id, ToString } from '../../../../fields/FieldSymbols'; import { ObjectField } from '../../../../fields/ObjectField'; import { RefField } from '../../../../fields/RefField'; @@ -237,7 +238,7 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do payload: val, }); val?.docs.forEach((doc, i) => { - const layoutDoc = Doc.Layout(doc); + const layoutDoc = doc[DocLayout]; let wid = pivotAxisWidth; let hgt = pivotAxisWidth / (Doc.NativeAspect(layoutDoc) || 1); if (hgt > pivotAxisWidth) { @@ -249,7 +250,7 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do y: -y + (pivotAxisWidth - hgt) / 2, width: wid, height: hgt, - backgroundColor: StrCast(layoutDoc.backgroundColor, 'white'), + backgroundColor: StrCast(doc.backgroundColor, 'white'), pair: { layout: doc }, replica: val.replicas[i], }); @@ -362,7 +363,7 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc: function layoutDocsAtTime(keyDocs: Doc[], key: number) { keyDocs.forEach(doc => { const stack = findStack(x, stacking); - const layoutDoc = Doc.Layout(doc); + const layoutDoc = doc[DocLayout]; let wid = pivotAxisWidth; let hgt = pivotAxisWidth / (Doc.NativeAspect(layoutDoc) || 1); if (hgt > pivotAxisWidth) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index cce0ff684..6c47a71b0 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -41,7 +41,7 @@ transition: background-color 1s ease 0s; } .collectionfreeformview-mask { - mix-blend-mode: multiply; + mix-blend-mode: hard-light; background-color: rgba(0, 0, 0, 0.8); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 7fd5d71fd..626538976 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -11,7 +11,7 @@ import ReactLoading from 'react-loading'; import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; -import { DocData, Height, Width } from '../../../../fields/DocSymbols'; +import { DocData, DocLayout, Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkEraserTool, InkField, InkInkTool, InkTool, Segment } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -286,7 +286,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image panX = () => this.fitContentBounds?.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(this.Document.freeform_panX, 1)); panY = () => this.fitContentBounds?.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(this.Document.freeform_panY, 1)); - zoomScaling = () => this.fitContentBounds?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); // , NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1)); + zoomScaling = () => this.fitContentBounds?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1); // , NumCast(DocCast(this.Document.rootDocument)?.[this.scaleFieldKey], 1)); PanZoomCenterXf = () => (this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.centeringShiftX}px, ${this.centeringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`); ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy(); getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); @@ -440,7 +440,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)]; docDragData.droppedDocuments.forEach((d, i) => { - const layoutDoc = Doc.Layout(d); + const layoutDoc = d[DocLayout]; const delta = Utils.rotPt(x - dropPos[0], y - dropPos[1], fromScreenXf.Rotate); if (this.Document._currentFrame !== undefined) { CollectionFreeFormDocumentView.setupKeyframes([d], NumCast(this.Document._currentFrame), false); @@ -1440,9 +1440,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => { - const layoutdoc = Doc.Layout(doc); const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y)); - const pt2 = xf.transformPoint(NumCast(doc.x) + NumCast(layoutdoc._width), NumCast(doc.y) + NumCast(layoutdoc._height)); + const pt2 = xf.transformPoint(NumCast(doc.x) + NumCast(doc._width), NumCast(doc.y) + NumCast(doc._height)); const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1], width: pt2[0] - pt[0], height: pt2[1] - pt[1] }; if (scale !== undefined) { @@ -1489,9 +1488,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection * @returns whether the new text doc was created and added successfully */ createTextDocCopy = undoable((textBox: FormattedTextBox, below: boolean) => { - const textDoc = DocCast(textBox.Document.rootDocument, textBox.Document); + const textDoc = DocCast(textBox.Document); const newDoc = Doc.MakeCopy(textDoc, true); - newDoc['$' + Doc.LayoutFieldKey(newDoc, textBox._props.LayoutTemplateString)] = undefined; // the copy should not copy the text contents of it source, just the render style + newDoc['$' + Doc.LayoutFieldKey(newDoc)] = undefined; // the copy should not copy the text contents of it source, just the render style newDoc.x = NumCast(textDoc.x) + (below ? 0 : NumCast(textDoc._width) + 10); newDoc.y = NumCast(textDoc.y) + (below ? NumCast(textDoc._height) + 10 : 0); DocumentView.SetSelectOnLoad(newDoc); @@ -1588,7 +1587,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } case undefined: case OpenWhere.lightbox: - if (this.layoutDoc._isLightbox) { + if (this.dataDoc.$isLightbox) { this._lightboxDoc = docs[0]; return true; } @@ -1600,14 +1599,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection 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); const childDoc = pair.layout; - const childDocLayout = Doc.Layout(childDoc); const layoutFrameNumber = Cast(this.Document._currentFrame, 'number'); // frame number that container is at which determines layout frame values - const contentFrameNumber = Cast(childDocLayout._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed - const { z, zIndex } = childDoc; + const contentFrameNumber = Cast(childDoc._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { z, zIndex, stroke_isInkMask } = childDoc; const { backgroundColor, color } = contentFrameNumber === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, contentFrameNumber); const { x, y, autoDim, _width, _height, opacity, _rotation } = layoutFrameNumber === undefined // -1 for width/height means width/height should be PanelWidth/PanelHeight (prevents collectionfreeformdocumentview width/height from getting out of synch with panelWIdth/Height which causes detailView to re-render and lose focus because HTMLtag scaling gets set to a bad intermediate value) - ? { autoDim: 1, _width: Cast(childDoc._width, 'number'), _height: Cast(childDoc._height, 'number'), _rotation: Cast(childDocLayout._rotation, 'number'), x: childDoc.x, y: childDoc.y, opacity: this._props.childOpacity?.() } + ? { autoDim: 1, _width: Cast(childDoc._width, 'number'), _height: Cast(childDoc._height, 'number'), _rotation: Cast(childDoc._rotation, 'number'), x: childDoc.x, y: childDoc.y, opacity: this._props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, layoutFrameNumber); // prettier-ignore const rotation = Cast(_rotation,'number', @@ -1625,8 +1624,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection zIndex: Cast(zIndex, 'number'), width: _width, height: _height, - transition: StrCast(childDocLayout.dataTransition), - showTags: BoolCast(childDocLayout.showTags) || BoolCast(this.Document.showChildTags) || BoolCast(this.Document._layout_showTags), + transition: StrCast(childDoc.dataTransition), + showTags: BoolCast(childDoc.showTags) || BoolCast(this.Document.showChildTags) || BoolCast(this.Document._layout_showTags), pointerEvents: Cast(childDoc.pointerEvents, 'string', null), pair, replica: '', @@ -1719,7 +1718,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection elements.push({ ele: this.getChildDocView(entry[1]), bounds: entry[1].opacity === 0 ? { payload: undefined, type: '', ...entry[1], width: 0, height: 0 } : { payload: undefined, type: '', ...entry[1] }, - inkMask: BoolCast(entry[1].pair.layout.stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1, + inkMask: BoolCast(entry[1].pair.layout?.$stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1, }) ); diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx index b40189d76..c7aa410c6 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -11,7 +11,7 @@ import { emptyFunction } from '../../../../Utils'; 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'; +import { DocCast, ImageCastToNameType, NumCast, StrCast } from '../../../../fields/Types'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; @@ -187,7 +187,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() { ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); })}> {FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => { - const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url.href.split('.') ?? ['-missing-', '.png']; + const [name, type] = ImageCastToNameType(doc[Doc.LayoutFieldKey(doc)]) ?? ['-missing-', '.png']; return ( <div className="image-wrapper" diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index c983d7c26..7c8ccb92d 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -10,7 +10,7 @@ import { imageUrlToBase64 } from '../../../../ClientUtils'; import { Utils, numberRange } from '../../../../Utils'; import { Doc, NumListCast, Opt } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; -import { ImageCast } from '../../../../fields/Types'; +import { ImageCastToNameType, ImageCastWithSuffix } from '../../../../fields/Types'; import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; @@ -165,8 +165,8 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { const imageInfos = this._selectedImages.map(async doc => { if (!doc.$tags_chat) { - const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); - return imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 => + const url = ImageCastWithSuffix(doc[Doc.LayoutFieldKey(doc)], '_o') ?? ''; + return imageUrlToBase64(url).then(hrefBase64 => !hrefBase64 ? undefined : gptImageLabel(hrefBase64,'Give three labels to describe this image.').then(labels => ({ doc, labels }))) ; // prettier-ignore @@ -199,13 +199,11 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { groupImagesInBox = action(async () => { this.startLoading(); - for (const doc of this._selectedImages) { - for (let index = 0; index < (doc.$tags_chat as List<string>).length; index++) { - const label = (doc.$tags_chat as List<string>)[index]; - const embedding = await gptGetEmbedding(label); - doc[`$tags_embedding_${index + 1}`] = new List<number>(embedding); - } - } + await Promise.all( + this._selectedImages + .map(doc => ({ doc, labels: doc.$tags_chat as List<string> })) + .map(({ doc, labels }) => labels.map((label, index) => gptGetEmbedding(label).then(embedding => (doc[`$tags_embedding_${index + 1}`] = new List<number>(embedding))))) + ); const labelToEmbedding = new Map<string, number[]>(); // Create embeddings for the labels. @@ -312,7 +310,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { {this._displayImageInformation ? ( <div className="image-information-list"> {this._selectedImages.map(doc => { - const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); + const [name, type] = ImageCastToNameType(doc[Doc.LayoutFieldKey(doc)]); return ( <div className="image-information" style={{ borderColor: SettingsManager.userColor }} key={Utils.GenerateGuid()}> <img diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index eaa8826ed..5fbf72f39 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -8,7 +8,7 @@ import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSy import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { GetEffectiveAcl } from '../../../../fields/util'; import { DocUtils } from '../../../documents/DocUtils'; @@ -18,7 +18,6 @@ import { SnappingManager, freeformScrollMode } from '../../../util/SnappingManag import { Transform } from '../../../util/Transform'; import { UndoManager, undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; -import { MainView } from '../../MainView'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { MarqueeViewBounds } from '../../PinFuncs'; import { PreviewCursor } from '../../PreviewCursor'; @@ -442,7 +441,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps if (groupButton) { this._selectedDocs = this.marqueeSelect(false, DocumentType.IMG); ImageLabelBoxData.Instance.setData(this._selectedDocs); - MainView.Instance.expandFlyout(groupButton); + ScriptCast(groupButton.onClick)?.script.run({ this: groupButton }); } }; @@ -591,8 +590,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps marqueeSelect = (selectBackgrounds: boolean = false, docType: DocumentType | undefined = undefined) => { const selection: Doc[] = []; const selectFunc = (doc: Doc) => { - const layoutDoc = Doc.Layout(doc); - const bounds = { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(layoutDoc._width), height: NumCast(layoutDoc._height) }; + const bounds = { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }; if (!this._lassoFreehand) { intersectRect(bounds, this.Bounds) && selection.push(doc); } else { diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index e7f1de7f1..a7603a45b 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -182,17 +182,17 @@ export class CollectionGridView extends CollectionSubView() { isChildContentActive = () => (this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) ? true : undefined); /** * - * @param layout + * @param childLayout * @param dxf the x- and y-translations of the decorations box as a transform i.e. this.lookupIndividualTransform * @param width * @param height * @returns the `ContentFittingDocumentView` of the node */ - getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => ( + getDisplayDoc = (childLayout: Doc, dxf: () => Transform, width: () => number, height: () => number) => ( <DocumentView {...this._props} - Document={layout} - TemplateDataDocument={layout.resolvedDataDoc as Doc} + Document={childLayout} + TemplateDataDocument={childLayout.isTemplateDoc || childLayout.isTemplateForField ? this._props.TemplateDataDocument : undefined} NativeWidth={returnZero} NativeHeight={returnZero} fitWidth={this._props.childLayoutFitWidth} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 8aae24df0..adaceb053 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -1,14 +1,17 @@ +import { Button, IconButton } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { Button, IconButton } from '@dash/components'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; +import { computedFn } from 'mobx-utils'; import * as React from 'react'; import { FaChevronRight } from 'react-icons/fa'; import { Doc, DocListCast } from '../../../../fields/Doc'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { DragManager } from '../../../util/DragManager'; +import { dropActionType } from '../../../util/DropActionTypes'; import { SettingsManager } from '../../../util/SettingsManager'; +import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoable } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; @@ -16,8 +19,6 @@ import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView' import './CollectionMulticolumnView.scss'; import ResizeBar from './MulticolumnResizer'; import WidthLabel from './MulticolumnWidthLabel'; -import { dropActionType } from '../../../util/DropActionTypes'; -import { SnappingManager } from '../../../util/SnappingManager'; interface WidthSpecifier { magnitude: number; @@ -197,16 +198,14 @@ export class CollectionMulticolumnView extends CollectionSubView() { * documents before the target. */ private lookupIndividualTransform = (layout: Doc) => { - if (this.columnUnitLength === undefined) { - return Transform.Identity(); // we're still waiting on promises to resolve - } - let offset = 0; - // eslint-disable-next-line no-restricted-syntax - for (const { layout: candidate } of this.childLayoutPairs) { - if (candidate === layout) { - return this.ScreenToLocalBoxXf().translate(-offset / (this._props.NativeDimScaling?.() || 1), 0); + if (this.columnUnitLength !== undefined) { + let offset = 0; + for (const { layout: candidate } of this.childLayoutPairs) { + if (candidate === layout) { + return this.ScreenToLocalBoxXf().translate(-offset / (this._props.NativeDimScaling?.() || 1), 0); + } + offset += this.lookupPixels(candidate) + resizerWidth; } - offset += this.lookupPixels(candidate) + resizerWidth; } return Transform.Identity(); }; @@ -262,50 +261,51 @@ export class CollectionMulticolumnView extends CollectionSubView() { ? true : undefined; }; - getDisplayDoc = (childLayout: Doc) => { - const width = () => this.lookupPixels(childLayout); - const height = () => this._props.PanelHeight() - 2 * NumCast(this.layoutDoc._yMargin) - (BoolCast(this.layoutDoc.showWidthLabels) ? 20 : 0); - const dxf = () => - this.lookupIndividualTransform(childLayout) + childHeight = () => this._props.PanelHeight() - 2 * NumCast(this.layoutDoc._yMargin) - (BoolCast(this.layoutDoc.showWidthLabels) ? 20 : 0); + childWidth = computedFn((childDoc: Doc) => () => this.lookupPixels(childDoc)); + childXf = computedFn( + (childDoc: Doc) => () => + this.lookupIndividualTransform(childDoc) .translate(-NumCast(this.layoutDoc._xMargin), -NumCast(this.layoutDoc._yMargin)) - .scale(this._props.NativeDimScaling?.() || 1); - return ( - <DocumentView - Document={childLayout} - TemplateDataDocument={childLayout.resolvedDataDoc as Doc} - styleProvider={this._props.styleProvider} - containerViewPath={this.childContainerViewPath} - LayoutTemplate={this._props.childLayoutTemplate} - LayoutTemplateString={this._props.childLayoutString} - renderDepth={this._props.renderDepth + 1} - PanelWidth={width} - PanelHeight={height} - rootSelected={this.rootSelected} - dragAction={StrCast(this.Document.childDragAction, this._props.childDragAction) as dropActionType} - onClickScript={this.onChildClickHandler} - onDoubleClickScript={this.onChildDoubleClickHandler} - suppressSetHeight - ScreenToLocalTransform={dxf} - isContentActive={this.isChildContentActive} - isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} - hideResizeHandles={!!(childLayout.layout_fitWidth || this._props.childHideResizeHandles)} - hideDecorationTitle={this._props.childHideDecorationTitle} - fitContentsToBox={this._props.fitContentsToBox} - focus={this._props.focus} - childFilters={this.childDocFilters} - childFiltersByRanges={this.childDocRangeFilters} - searchFilterDocs={this.searchFilterDocs} - dontRegisterView={this._props.dontRegisterView} - addDocument={this._props.addDocument} - moveDocument={this._props.moveDocument} - removeDocument={this._props.removeDocument} - whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} - addDocTab={this._props.addDocTab} - pinToPres={this._props.pinToPres} - dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy'} - /> - ); - }; + .scale(this._props.NativeDimScaling?.() || 1) + ); + getDisplayDoc = (childLayout: Doc) => ( + <DocumentView + Document={childLayout} + TemplateDataDocument={childLayout.isTemplateDoc || childLayout.isTemplateForField ? this._props.TemplateDataDocument : undefined} + styleProvider={this._props.styleProvider} + containerViewPath={this.childContainerViewPath} + LayoutTemplate={this._props.childLayoutTemplate} + LayoutTemplateString={this._props.childLayoutString} + renderDepth={this._props.renderDepth + 1} + PanelWidth={this.childWidth(childLayout)} + PanelHeight={this.childHeight} + rootSelected={this.rootSelected} + dragAction={StrCast(this.Document.childDragAction, this._props.childDragAction) as dropActionType} + onClickScript={this.onChildClickHandler} + onDoubleClickScript={this.onChildDoubleClickHandler} + suppressSetHeight + ScreenToLocalTransform={this.childXf(childLayout)} + isContentActive={this.isChildContentActive} + isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} + hideResizeHandles={!!(childLayout.layout_fitWidth || this._props.childHideResizeHandles)} + hideDecorationTitle={this._props.childHideDecorationTitle} + fitContentsToBox={this._props.fitContentsToBox} + focus={this._props.focus} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + dontRegisterView={this._props.dontRegisterView} + addDocument={this._props.addDocument} + moveDocument={this._props.moveDocument} + removeDocument={this._props.removeDocument} + whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} + addDocTab={this._props.addDocTab} + pinToPres={this._props.pinToPres} + dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy'} + /> + ); + /** * @returns the resolved list of rendered child documents, displayed * at their resolved pixel widths, each separated by a resizer. @@ -315,7 +315,6 @@ export class CollectionMulticolumnView extends CollectionSubView() { const collector: JSX.Element[] = []; this.childLayouts.forEach((layout, i) => { collector.push( - // eslint-disable-next-line react/no-array-index-key <Tooltip title={'Doc: ' + StrCast(layout.title)} key={'wrapper' + i}> <div className="document-wrapper" style={{ flexDirection: 'column', width: this.lookupPixels(layout) }}> {this.getDisplayDoc(layout)} @@ -327,7 +326,6 @@ export class CollectionMulticolumnView extends CollectionSubView() { </Tooltip>, <ResizeBar width={resizerWidth} - // eslint-disable-next-line react/no-array-index-key key={'resizer' + i} styleProvider={this._props.styleProvider} isContentActive={this._props.isContentActive} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index bda8e91ac..27c41583c 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -1,5 +1,6 @@ import { action, computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; +import { computedFn } from 'mobx-utils'; import * as React from 'react'; import { Doc, DocListCast } from '../../../../fields/Doc'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; @@ -178,16 +179,14 @@ export class CollectionMultirowView extends CollectionSubView() { * documents before the target. */ private lookupIndividualTransform = (layout: Doc) => { - if (this.rowUnitLength === undefined) { - return Transform.Identity(); // we're still waiting on promises to resolve - } - let offset = 0; - // eslint-disable-next-line no-restricted-syntax - for (const { layout: candidate } of this.childLayoutPairs) { - if (candidate === layout) { - return this.ScreenToLocalBoxXf().translate(0, -offset / (this._props.NativeDimScaling?.() || 1)); + if (this.rowUnitLength !== undefined) { + let offset = 0; + for (const { layout: candidate } of this.childLayoutPairs) { + if (candidate === layout) { + return this.ScreenToLocalBoxXf().translate(0, -offset / (this._props.NativeDimScaling?.() || 1)); + } + offset += this.lookupPixels(candidate) + resizerHeight; } - offset += this.lookupPixels(candidate) + resizerHeight; } return Transform.Identity(); // type coersion, this case should never be hit }; @@ -243,49 +242,51 @@ export class CollectionMultirowView extends CollectionSubView() { ? true : undefined; }; - getDisplayDoc = (layout: Doc) => { - const height = () => this.lookupPixels(layout); - const width = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc._xMargin) - (BoolCast(this.layoutDoc.showWidthLabels) ? 20 : 0); - const dxf = () => - this.lookupIndividualTransform(layout) + childHeight = computedFn((childDoc: Doc) => () => this.lookupPixels(childDoc)); + childWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc._xMargin) - (BoolCast(this.layoutDoc.showWidthLabels) ? 20 : 0); + childXf = computedFn( + (childDoc: Doc) => () => + this.lookupIndividualTransform(childDoc) .translate(-NumCast(this.layoutDoc._xMargin), -NumCast(this.layoutDoc._yMargin)) - .scale(this._props.NativeDimScaling?.() || 1); - return ( - <DocumentView - Document={layout} - TemplateDataDocument={layout.resolvedDataDoc as Doc} - styleProvider={this._props.styleProvider} - containerViewPath={this.childContainerViewPath} - LayoutTemplate={this._props.childLayoutTemplate} - LayoutTemplateString={this._props.childLayoutString} - renderDepth={this._props.renderDepth + 1} - PanelWidth={width} - PanelHeight={height} - rootSelected={this.rootSelected} - dragAction={StrCast(this.Document.childDragAction, this._props.childDragAction) as dropActionType} - onClickScript={this.onChildClickHandler} - onDoubleClickScript={this.onChildDoubleClickHandler} - ScreenToLocalTransform={dxf} - isContentActive={this.isChildContentActive} - isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} - hideResizeHandles={!!(layout.layout_fitWidth || this._props.childHideResizeHandles)} - hideDecorationTitle={this._props.childHideDecorationTitle} - fitContentsToBox={this._props.fitContentsToBox} - focus={this._props.focus} - childFilters={this.childDocFilters} - childFiltersByRanges={this.childDocRangeFilters} - searchFilterDocs={this.searchFilterDocs} - dontRegisterView={this._props.dontRegisterView} - addDocument={this._props.addDocument} - moveDocument={this._props.moveDocument} - removeDocument={this._props.removeDocument} - whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} - addDocTab={this._props.addDocTab} - pinToPres={this._props.pinToPres} - dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'y' | 'x' | 'xy'} - /> - ); - }; + .scale(this._props.NativeDimScaling?.() || 1) + ); + + getDisplayDoc = (childLayout: Doc) => ( + <DocumentView + Document={childLayout} + TemplateDataDocument={childLayout.isTemplateDoc || childLayout.isTemplateForField ? this._props.TemplateDataDocument : undefined} + styleProvider={this._props.styleProvider} + containerViewPath={this.childContainerViewPath} + LayoutTemplate={this._props.childLayoutTemplate} + LayoutTemplateString={this._props.childLayoutString} + renderDepth={this._props.renderDepth + 1} + PanelWidth={this.childWidth} + PanelHeight={this.childHeight(childLayout)} + rootSelected={this.rootSelected} + dragAction={StrCast(this.Document.childDragAction, this._props.childDragAction) as dropActionType} + onClickScript={this.onChildClickHandler} + onDoubleClickScript={this.onChildDoubleClickHandler} + ScreenToLocalTransform={this.childXf(childLayout)} + isContentActive={this.isChildContentActive} + isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} + hideResizeHandles={!!(childLayout.layout_fitWidth || this._props.childHideResizeHandles)} + hideDecorationTitle={this._props.childHideDecorationTitle} + fitContentsToBox={this._props.fitContentsToBox} + focus={this._props.focus} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + dontRegisterView={this._props.dontRegisterView} + addDocument={this._props.addDocument} + moveDocument={this._props.moveDocument} + removeDocument={this._props.removeDocument} + whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} + addDocTab={this._props.addDocTab} + pinToPres={this._props.pinToPres} + dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'y' | 'x' | 'xy'} + /> + ); + /** * @returns the resolved list of rendered child documents, displayed * at their resolved pixel widths, each separated by a resizer. diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 05670562e..5803acca0 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -200,7 +200,7 @@ export class CollectionSchemaView extends CollectionSubView() { this._props.setContentViewBox?.(this); document.addEventListener('keydown', this.onKeyDown); - Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => this.fieldInfos.set(pair[0], pair[1])); + Object.entries(this._documentOptions).forEach(pair => this.fieldInfos.set(pair[0], pair[1] as FInfo)); this._keysDisposer = observe( this.dataDoc[this.fieldKey ?? 'data'] as List<Doc>, change => { diff --git a/src/client/views/collections/collectionSchema/SchemaCellField.tsx b/src/client/views/collections/collectionSchema/SchemaCellField.tsx index daffdf1f5..9ad94cb31 100644 --- a/src/client/views/collections/collectionSchema/SchemaCellField.tsx +++ b/src/client/views/collections/collectionSchema/SchemaCellField.tsx @@ -85,6 +85,7 @@ export class SchemaCellField extends ObservableReactComponent<SchemaCellFieldPro () => this._editing, editing => { if (editing) { + this.setContent((this._unrenderedContent = this._props.GetValue() ?? '')); this.setupRefSelect(this.refSelectConditionMet); } else { this._overlayDisposer?.(); @@ -99,7 +100,7 @@ export class SchemaCellField extends ObservableReactComponent<SchemaCellFieldPro () => this._props.GetValue(), fieldVal => { this._unrenderedContent = fieldVal ?? ''; - this.finalizeEdit(false, false, false); + this._editing && this.finalizeEdit(false, false, false); } ); } diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index e6fe46638..173984dc7 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -32,6 +32,7 @@ import { FInfotoColType } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaCellField } from './SchemaCellField'; +import { DocLayout } from '../../../../fields/DocSymbols'; /** * SchemaTableCells make up the majority of the visual representation of the SchemaView. @@ -104,13 +105,14 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro public static renderProps(props: SchemaTableCellProps) { const { Doc: Document, fieldKey, /* getFinfo,*/ columnWidth, isRowActive } = props; let protoCount = 0; - let doc: Doc | undefined = Document; + const layoutDoc = fieldKey.startsWith('_') ? Document[DocLayout] : Document; + let doc = Document; while (doc) { if (Object.keys(doc).includes(fieldKey.replace(/^_/, ''))) break; protoCount++; doc = DocCast(doc.proto); } - const color = protoCount === 0 || (fieldKey.startsWith('_') && Document[fieldKey] === undefined) ? 'black' : 'blue'; // color of text in cells + const color = layoutDoc !== Document ? 'red' : protoCount === 0 || (fieldKey.startsWith('_') && Document[fieldKey] === undefined) ? 'black' : 'blue'; // color of text in cells const textDecoration = ''; const fieldProps: FieldViewProps = { childFilters: returnEmptyFilter, @@ -130,7 +132,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro focus: emptyFunction, addDocTab: SchemaTableCell.addFieldDoc, pinToPres: returnZero, - Document: DocCast(Document.rootDocument, Document), + Document: Document, fieldKey: fieldKey, PanelWidth: columnWidth, PanelHeight: props.rowHeight, @@ -175,7 +177,8 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro const inQuotes = (strField: string) => { return (strField.startsWith('`') && strField.endsWith('`')) || (strField.startsWith("'") && strField.endsWith("'")) || (strField.startsWith('"') && strField.endsWith('"')); }; - if (!inQuotes(this._submittedValue) && inQuotes(modField)) modField = modField.substring(1, modField.length - 1); + const submittedValue = this._submittedValue.startsWith(eqSymbol) ? this._submittedValue.slice(eqSymbol.length) : this._submittedValue; + if (!inQuotes(submittedValue) && inQuotes(modField)) modField = modField.substring(1, modField.length - 1); return eqSymbol + modField; }; @@ -211,8 +214,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro this._props.finishEdit?.(); return true; } - const hasNoLayout = Doc.IsDataProto(fieldProps.Document) ? true : undefined; // the "delegate" is a a data document so never write to it's proto - const ret = Doc.SetField(fieldProps.Document, this._props.fieldKey.replace(/^_/, ''), value, hasNoLayout); + const ret = Doc.SetField(fieldProps.Document, this._props.fieldKey, value); this._submittedValue = value; this._props.finishEdit?.(); return ret; diff --git a/src/client/views/global/globalCssVariables.module.scss b/src/client/views/global/globalCssVariables.module.scss index ad0c5c21d..82f6caa52 100644 --- a/src/client/views/global/globalCssVariables.module.scss +++ b/src/client/views/global/globalCssVariables.module.scss @@ -4,7 +4,7 @@ $white: #ffffff; $off-white: #fdfdfd; $light-gray: #dfdfdf; $medium-gray: #9f9f9f; -$medium-gray-dim: #9f9f9f30; +$medium-gray-dim: #9f9f9f; $dark-gray: #323232; $black: #000000; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 79d0ee88f..32afb3d3d 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -38,6 +38,7 @@ import { VideoBox } from '../nodes/VideoBox'; import { WebBox } from '../nodes/WebBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup'; +import { DocData } from '../../../fields/DocSymbols'; // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function IsNoneSelected() { @@ -119,7 +120,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b 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] || defaultFill(); + return (contentFrameNumber !== undefined ? CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] : selView.backgroundColor()) || defaultFill(); } !selectedViews.length && setDefaultFill(color ?? 'transparent'); selectedViews.forEach(dv => { @@ -131,10 +132,10 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b obj[fieldKey] = color; CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.Document, obj); } else { - const dataKey = Doc.LayoutFieldKey(dv.Document); + const colorDoc = dv.isTemplateForField ? dv.layoutDoc : dv.dataDoc; // assigning to a template's compoment field should not assign to the data doc + const dataKey = Doc.LayoutFieldKey(colorDoc); const alternate = (dv.layoutDoc[dataKey + '_usePath'] ? '_' + dv.layoutDoc[dataKey + '_usePath'] : '').replace(':hover', ''); - dv.layoutDoc[fieldKey + alternate] = undefined; - dv.dataDoc[fieldKey + alternate] = color; + colorDoc[fieldKey + alternate] = color; } }); } else { @@ -367,8 +368,8 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: toggle: () => editorView?.state && RichTextMenu.Instance?.changeListType(list) }]); // prettier-ignore const attrs:attrfuncs[] = [ - ['dictation', { checkResult: () => !!textView?._recordingDictation, - toggle: () => textView && runInAction(() => { textView._recordingDictation = !textView._recordingDictation;} ) }], + ['dictation', { checkResult: () => !!textView?.recordingDictation, + toggle: () => textView && runInAction(() => { textView.recordingDictation = !textView.recordingDictation;} ) }], ['fitBox', { checkResult: () => RichTextMenu.Instance?.fitBox ?? false, toggle: () => RichTextMenu.Instance?.toggleFitBox()}], ['elide', { checkResult: () => false, diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 6f86383c2..53a3d3631 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -17,9 +17,10 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { DocComponent } from '../DocComponent'; import { StyleProp } from '../StyleProp'; import './CollectionFreeFormDocumentView.scss'; -import { DocumentView, DocumentViewProps } from './DocumentView'; +import { DocumentView } from './DocumentView'; import { FieldViewProps } from './FieldView'; import { OpenWhere } from './OpenWhere'; +import { DocumentViewProps } from './DocumentContentsView'; export enum GroupActive { // flags for whether a view is activate because of its relationship to a group group = 'group', // this is a group that is activated because it's on an active canvas, but is not part of some other group @@ -304,7 +305,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF ) : ( <DocumentView {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore - Document={this._renderDoc} + Document={this.Document} renderDepth={this._props.renderDepth} isContentActive={this._props.isContentActive} childFilters={this._props.childFilters} diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 992fbba66..6d5891003 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -29,7 +29,6 @@ import '../pdf/GPTPopup/GPTPopup.scss'; import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { TraceMobx } from '../../../fields/util'; const API_URL = 'https://api.unsplash.com/search/photos'; @@ -285,13 +284,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }; @action handleRenderGPTClick = () => { - const phonTrans = DocCast(this.Document.audio) ? DocCast(this.Document.audio).phoneticTranscription : undefined; - if (phonTrans) { - this._inputValue = StrCast(phonTrans); - this.askGPTPhonemes(this._inputValue); - this._renderSide = this.backKey; - this._outputValue = ''; - } else if (this._inputValue) this.askGPT(GPTCallType.QUIZDOC); + if (this._inputValue) this.askGPT(GPTCallType.QUIZDOC); }; onPointerMove = ({ movementX }: PointerEvent) => { @@ -468,45 +461,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }; /** - * Gets the transcription of an audio recording by sending the - * recording to backend. - */ - pushInfo = () => - axios - .post( - 'http://localhost:105/recognize/', // - { file: DocCast(this.Document.audio).$url }, - { headers: { 'Content-Type': 'application/json' } } - ) - .then(response => { - this.Document.phoneticTranscription = response.data.transcription; - }); - - /** - * Extracts the id of the youtube video url. - * @param url - * @returns - */ - getYouTubeVideoId = (url: string) => { - const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=|\?v=)([^#&?]*).*/; - const match = url.match(regExp); - return match && match[2].length === 11 ? match[2] : null; - }; - - /** - * Gets the transcript of a youtube video by sending the video url to the backend. - * @returns transcription of youtube recording - */ - youtubeUpload = async () => - axios - .post( - 'http://localhost:105/youtube/', // - { file: this.getYouTubeVideoId(this.frontText) }, - { headers: { 'Content-Type': 'application/json' } } - ) - .then(response => response.data.transcription); - - /** * Calls GPT for each flashcard type. */ askGPT = async (callType: GPTCallType) => { @@ -540,45 +494,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() layoutHeight = () => NumCast(this.layoutDoc.height, 200); /** - * Ask GPT for advice on how to improve speech by comparing the phonetic transcription of - * a users audio recording with the phonetic transcription of their intended sentence. - * @param phonemes - */ - askGPTPhonemes = async (phonemes: string) => { - const sentence = this.frontText; - const phon6 = 'huː ɑɹ juː tədeɪ'; - const phon4 = 'kamo estas hɔi'; - const promptEng = - 'Consider all possible phonetic transcriptions of the intended sentence "' + - sentence + - '" that is standard in American speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + - phon6 + - '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in American speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Only note the difference if they are not allophones of the same phoneme and if they are far away on the vowel chart. The goal is to be understood, not sound like a native speaker. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Provide a response like this: "Lets work on improving the pronunciation of "coffee." You said "ceeffee," which is close, but we need to adjust the vowel sound. In American English, "coffee" is pronounced /ˈkɔːfi/, with a long "aw" sound. Try saying "kah-fee." Your intonation is good, but try putting a bit more stress on "like" in the sentence "I would like a coffee with milk." This will make your speech sound more natural. Keep practicing, and lets try saying the whole sentence again!"'; - const promptSpa = - 'Consider all possible phonetic transcriptions of the intended sentence "' + - 'como estás hoy' + - '" that is standard in Spanish speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + - phon4 + - '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in Spanish speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Only note the difference if they are not allophones of the same phoneme and if they are far away on the vowel chart; say good job if it would be understood by a native Spanish speaker. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". Do not make "θ" and "f" interchangable. Do not make "n" and "ɲ" interchangable. Do not make "e" and "i" interchangable. If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Identify "ɔi" sounds like "oy". Ignore accents and do not say anything to the user about this.'; - const promptAll = - 'Consider all possible phonetic transcriptions of the intended sentence "' + - sentence + - '" that is standard in ' + - this.convertAbr() + - ' speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + - phonemes + - '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in ' + - this.convertAbr() + - ' speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". Do not make "θ" and "f" interchangable. Do not make "n" and "ɲ" interchangable. Do not make "e" and "i" interchangable. If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Provide a response like this: "Lets work on improving the pronunciation of "coffee." You said "cawffee," which is close, but we need to adjust the vowel sound. In American English, "coffee" is pronounced /ˈkɔːfi/, with a long "aw" sound. Try saying "kah-fee." Your intonation is good, but try putting a bit more stress on "like" in the sentence "I would like a coffee with milk." This will make your speech sound more natural. Keep practicing, and lets try saying the whole sentence again!"'; - - switch (this._recognition.lang) { - case 'en-US': this._outputValue = await gptAPICall(promptEng, GPTCallType.PRONUNCIATION); break; - case 'es-ES': this._outputValue = await gptAPICall(promptSpa, GPTCallType.PRONUNCIATION); break; - default: this._outputValue = await gptAPICall(promptAll, GPTCallType.PRONUNCIATION); break; - } // prettier-ignore - }; - - /** * Display a user's speech to text result. * @param e */ @@ -637,31 +552,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); }; - testForTextFields = (whichSlot: string) => { - const slotData = Doc.Get(this.dataDoc, whichSlot, true); - const slotHasText = slotData instanceof RichTextField || typeof slotData === 'string'; - const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim(); - const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim(); - const layoutTemplateString = - slotHasText ? FormattedTextBox.LayoutString(whichSlot): - whichSlot === this.frontKey ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) : - altText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore - - // A bit hacky to try out the concept of using GPT to fill in flashcards - // If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string) - // and the fieldKey + "_alternate" has text that includes a GPT query (indicated by (( && )) ) that is parameterized (optionally) by the fieldKey text (this) or other metadata (this.<field>). - // eg., this.text_alternate is - // "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))" - // where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field - // The GPT call will put the "answer" in the second slot of the comparison (eg., text_0) - if (whichSlot === this.backKey && !layoutTemplateString?.includes(whichSlot)) { - const queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in Doc.setField but it doesn't know about the fieldKey ... - if (queryText?.match(/\(\(.*\)\)/)) { - Doc.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt - } - } - return layoutTemplateString; - }; childActiveFunc = () => this._childActive; contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); @@ -682,24 +572,21 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() displayDoc = (whichSlot: string) => { const whichDoc = DocCast(this.dataDoc[whichSlot]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); - return targetDoc || layoutString ? ( + return targetDoc ? ( <> <DocumentView {...this._props} - Document={layoutString ? this.Document : targetDoc} + Document={targetDoc} NativeWidth={returnZero} NativeHeight={returnZero} renderDepth={this.props.renderDepth + 1} - LayoutTemplateString={layoutString} containerViewPath={this._props.docViewPath} ScreenToLocalTransform={this.contentScreenToLocalXf} isDocumentActive={returnFalse} isContentActive={this.childActiveFunc} showTags={undefined} fitWidth={this.childFitWidth} // set to returnTrue to make images fill the comparisonBox-- should be a user option - ignoreUsePath={layoutString ? true : undefined} moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack} removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack} dontSelect={returnTrue} @@ -745,18 +632,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() </div> <div> <div className="submit-button"> - {/* <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, false)}> - <FontAwesomeIcon color="white" icon="caret-down" /> - </div> */} - {/* <button className="submit-buttonrecord" onClick={this._listening ? this.stopListening : this.startListening} style={{ background: this._listening ? 'lightgray' : '' }}> - {<FontAwesomeIcon icon="microphone" size="lg" />} - </button> */} - {/* <div className="submit-buttonschema-header-button" onPointerDown={e => this.openContextMenu(e.clientX, e.clientY, true)} style={{ left: '50px', zIndex: '100' }}> - <FontAwesomeIcon color="white" icon="caret-down" /> - </div> */} - {/* <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}> - Evaluate Pronunciation - </button> */} <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.backKey ? () => this.animateFlipping(this.frontKey) : this.handleRenderGPTClick}> {this._renderSide === this.backKey ? 'Redo the Question' : 'Submit'} </button> diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 47c5734f7..6f004bed3 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -11,10 +11,85 @@ import { Cast, DocCast, StrCast } from '../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { ObservableReactComponent, ObserverJsxParser } from '../ObservableReactComponent'; import './DocumentView.scss'; -import { FieldViewProps } from './FieldView'; +import { FieldViewProps, FieldViewSharedProps } from './FieldView'; +import { DragManager } from '../../util/DragManager'; +import { dropActionType } from '../../util/DropActionTypes'; +import { Property } from 'csstype'; + +interface DocOnlyProps { + LayoutTemplate?: () => Opt<Doc>; + LayoutTemplateString?: string; + hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected + hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected + hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + hideDocumentButtonBar?: boolean; + hideOpenButton?: boolean; + hideDeleteButton?: boolean; + hideLinkAnchors?: boolean; + hideLinkButton?: boolean; + hideCaptions?: boolean; + contentPointerEvents?: Property.PointerEvents | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents + dontCenter?: 'x' | 'y' | 'xy'; + showTags?: boolean; + showAIEditor?: boolean; + hideFilterStatus?: boolean; + childHideDecorationTitle?: boolean; + childHideResizeHandles?: boolean; + childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. + dragWhenActive?: boolean; + dontHideOnDrag?: boolean; + onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected + DataTransition?: () => string | undefined; + NativeWidth?: () => number; + NativeHeight?: () => number; + contextMenuItems?: () => { script?: ScriptField; method?: () => void; filter?: ScriptField; label: string; icon: string }[]; + dragConfig?: (data: DragManager.DocumentDragData) => void; + dragStarting?: () => void; + dragEnding?: () => void; + + reactParent?: React.Component; // parent React component view (see CollectionFreeFormDocumentView) +} +const DocOnlyProps = [ + 'layoutFieldKey', + 'LayoutTemplate', + 'LayoutTemplateString', + 'hideDecorations', // whether to suppress all DocumentDecorations when doc is selected + 'hideResizeHandles', // whether to suppress resized handles on doc decorations when this document is selected + 'hideTitle', // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + 'hideDecorationTitle', // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + 'hideDocumentButtonBar', + 'hideOpenButton', + 'hideDeleteButton', + 'hideLinkAnchors', + 'hideLinkButton', + 'hideCaptions', + 'contentPointerEvents', // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents + 'dontCenter', + 'showTags', + 'showAIEditor', + 'hideFilterStatus', + 'childHideDecorationTitle', + 'childHideResizeHandles', + 'childDragAction', // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. + 'dragWhenActive', + 'dontHideOnDrag', + 'onClickScriptDisable', // undefined = only when selected + 'DataTransition', + 'NativeWidth', + 'NativeHeight', + 'contextMenuItems', + 'dragConfig', + 'dragStarting', + 'dragEnding', + + 'reactParent', // parent React component view (see CollectionFreeFormDocumentView) +]; + +export interface DocumentViewProps extends DocOnlyProps, FieldViewSharedProps {} type BindingProps = Without<FieldViewProps, 'fieldKey'>; -export interface JsxBindings { +interface JsxBindings { props: BindingProps; } @@ -77,7 +152,7 @@ export class HTMLtag extends React.Component<HTMLtagProps> { } } -export interface DocumentContentsViewProps extends FieldViewProps { +interface DocumentContentsViewProps extends DocumentViewProps, FieldViewProps { layoutFieldKey: string; } @observer @@ -113,29 +188,11 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte this._props.LayoutTemplate?.() || (this._props.LayoutTemplateString && this._props.Document) || (this._props.layoutFieldKey && StrCast(this._props.Document[this._props.layoutFieldKey]) && this._props.Document) || - Doc.Layout(this._props.Document, this._props.layoutFieldKey ? Cast(this._props.Document[this._props.layoutFieldKey], Doc, null) : undefined); + Doc.Layout(this._props.Document, DocCast(this._props.Document[this._props.layoutFieldKey])); return Doc.expandTemplateLayout(template, this._props.Document); } CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings { - const docOnlyProps = [ - // these are the properties in DocumentViewProps that need to be removed to pass on only DocumentSharedViewProps to the FieldViews - 'hideResizeHandles', - 'hideTitle', - 'bringToFront', - 'childContentPointerEvents', - 'LayoutTemplateString', - 'LayoutTemplate', - 'showTags', - 'layoutFieldKey', - 'dontCenter', - 'DataTransition', - 'contextMenuItems', - // 'onClick', // don't need to omit this since it will be set - 'onDoubleClickScript', - 'onPointerDownScript', - 'onPointerUpScript', - ]; const templateDataDoc = this._props.TemplateDataDocument ?? (this.layoutDoc !== this._props.Document ? this._props.Document[DocData] : undefined); const list: BindingProps & React.DetailedHTMLProps<React.HtmlHTMLAttributes<HTMLDivElement>, HTMLDivElement> = { ...this._props, @@ -146,7 +203,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte }; return { props: { - ...OmitKeys(list, [...docOnlyProps], '').omit, + ...OmitKeys(list, DocOnlyProps, '').omit, } as BindingProps, }; } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index dd5fd0d0c..5ac66f2cd 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -273,7 +273,15 @@ .documentView-noAIWidgets { transform-origin: top left; - position: relative; + position: absolute; + bottom: 0; + pointer-events: none; +} +.documentView-widgetDecorations { + transform-origin: top right; + position: absolute; + top: 0; + right: 0; } .documentView-editorView-history { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c355e57d4..bec5ea5c1 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -43,10 +43,10 @@ import { StyleProp } from '../StyleProp'; import { TagsView } from '../TagsView'; import { ViewBoxInterface } from '../ViewBoxInterface'; import { GroupActive } from './CollectionFreeFormDocumentView'; -import { DocumentContentsView } from './DocumentContentsView'; +import { DocumentContentsView, DocumentViewProps } from './DocumentContentsView'; import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; -import { FieldViewProps, FieldViewSharedProps } from './FieldView'; +import { FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import { OpenWhere, OpenWhereMod } from './OpenWhere'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; @@ -54,41 +54,10 @@ import { PresEffect, PresEffectDirection } from './trails/PresEnums'; import SpringAnimation from './trails/SlideEffect'; import { SpringType, springMappings } from './trails/SpringUtils'; -export interface DocumentViewProps extends FieldViewSharedProps { - hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected - hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected - hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings - hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings - hideDocumentButtonBar?: boolean; - hideOpenButton?: boolean; - hideDeleteButton?: boolean; - hideLinkAnchors?: boolean; - hideLinkButton?: boolean; - hideCaptions?: boolean; - contentPointerEvents?: Property.PointerEvents | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents - dontCenter?: 'x' | 'y' | 'xy'; - showTags?: boolean; - hideFilterStatus?: boolean; - childHideDecorationTitle?: boolean; - childHideResizeHandles?: boolean; - childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. - dragWhenActive?: boolean; - dontHideOnDrag?: boolean; - onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected - DataTransition?: () => string | undefined; - NativeWidth?: () => number; - NativeHeight?: () => number; - contextMenuItems?: () => { script?: ScriptField; method?: () => void; filter?: ScriptField; label: string; icon: string }[]; - dragConfig?: (data: DragManager.DocumentDragData) => void; - dragStarting?: () => void; - dragEnding?: () => void; - - reactParent?: React.Component; // parent React component view (see CollectionFreeFormDocumentView) -} @observer -export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps & { showAIEditor: boolean }>() { +export class DocumentViewInternal extends DocComponent<DocumentViewProps & FieldViewProps>() { // this makes mobx trace() statements more descriptive - public get displayName() { return 'DocumentViewInternal(' + this.Document.$title + ')'; } // prettier-ignore + public get displayName() { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. /** @@ -134,14 +103,14 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @computed get border() { return this.style(this.layoutDoc, StyleProp.Border) as string || ""; } // prettier-ignore @computed get borderRounding() { return this.style(this.layoutDoc, StyleProp.BorderRounding) as string; } // prettier-ignore @computed get widgetDecorations() { return this.style(this.layoutDoc, StyleProp.Decorations) as JSX.Element; } // prettier-ignore - @computed get backgroundBoxColor(){ return this.style(this.Document, StyleProp.BackgroundColor + ':docView') as string; } // prettier-ignore @computed get showTitle() { return this.style(this.layoutDoc, StyleProp.ShowTitle) as Opt<string>; } // prettier-ignore @computed get showCaption() { return this.style(this.layoutDoc, StyleProp.ShowCaption) as string ?? ""; } // prettier-ignore @computed get headerMargin() { return this.style(this.layoutDoc, StyleProp.HeaderMargin) as number ?? 0; } // prettier-ignore @computed get titleHeight() { return this.style(this.layoutDoc, StyleProp.TitleHeight) as number ?? 0; } // prettier-ignore - @computed get docContents() { return this.style(this.Document, StyleProp.DocContents) as JSX.Element; } // prettier-ignore - @computed get highlighting() { return this.style(this.Document, StyleProp.Highlighting); } // prettier-ignore - @computed get borderPath() { return this.style(this.Document, StyleProp.BorderPath); } // prettier-ignore + @computed get backgroundBoxColor(){ return this.style(this.Document, StyleProp.BackgroundColor + ':docView') as string; } // prettier-ignore + @computed get docContents() { return this.style(this.Document, StyleProp.DocContents) as JSX.Element; } // prettier-ignore + @computed get highlighting() { return this.style(this.Document, StyleProp.Highlighting); } // prettier-ignore + @computed get borderPath() { return this.style(this.Document, StyleProp.BorderPath); } // prettier-ignore @computed get onClickHdlr() { return this._props.onClickScript?.() ?? ScriptCast(this.layoutDoc.onClick ?? this.Document.onClick); } // prettier-ignore @computed get onDoubleClickHdlr() { return this._props.onDoubleClickScript?.() ?? ScriptCast(this.layoutDoc.onDoubleClick ?? this.Document.onDoubleClick); } // prettier-ignore @@ -322,7 +291,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document DocumentView.DeselectAll(); Doc.UnBrushDoc(this.Document); } else this._singleClickFunc?.(); - }, 'on double click: ' + this.Document.$title)(); + }, 'on double click: ' + this.Document.title)(); this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); this._doubleClickTimeout = undefined; this._singleClickFunc = undefined; @@ -340,7 +309,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (this.layoutDoc.onDragStart && !(e.ctrlKey || e.button > 0)) stopPropagate = false; preventDefault = false; } - this._singleClickFunc = undoable(clickFunc ?? sendToBack ?? selectFunc, 'click: ' + this.Document.$title); + this._singleClickFunc = undoable(clickFunc ?? sendToBack ?? selectFunc, 'click: ' + this.Document.title); const waitForDblClick = this._props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; if ((clickFunc && waitForDblClick !== 'never') || waitForDblClick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); @@ -519,7 +488,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (e && !(e.nativeEvent instanceof simMouseEvent ? e.nativeEvent.dash : false)) { const onDisplay = () => { - if (this.Document.$type !== DocumentType.MAP) DocumentViewInternal.SelectAfterContextMenu && this._props.select(false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. + if (this.Document.type !== DocumentType.MAP) DocumentViewInternal.SelectAfterContextMenu && this._props.select(false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. setTimeout(() => simulateMouseClick(document.elementFromPoint(e.clientX, e.clientY), e.clientX, e.clientY, e.screenX, e.screenY)); }; if (navigator.userAgent.includes('Macintosh')) { @@ -545,7 +514,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document cm.addItem({ description: item.label, event: () => (item.method ? item.method() : item.script?.script.run({ this: this.Document, documentView: this, scriptContext: this._props.scriptContext })), icon: item.icon as IconProp }) ); - if (!this.Document.$isFolder) { + if (!this.Document.isFolder) { const templateDoc = Cast(this.Document[StrCast(this.Document.layout_fieldKey)], Doc, null); const appearance = cm.findByDescription('Appearance...'); const appearanceItems = appearance?.subitems ?? []; @@ -635,7 +604,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document let documentationDescription: string | undefined; let documentationLink: string | undefined; - switch (this.Document.$type) { + switch (this.Document.type) { case DocumentType.COL: documentationDescription = 'See collection documentation'; documentationLink = 'https://brown-dash.github.io/Dash-Documentation/views/'; @@ -692,7 +661,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document .ScreenToLocalTransform() .translate(-NumCast(this.Document.borderWidth), -this.headerMargin - NumCast(this.Document.borderWidth)) .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.aiContentsHeight() : 1); - onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr; + onClickFunc = () => (this.disableClickScriptFunc ? undefined : this.onClickHdlr); setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height + 2 * NumCast(this.Document.borderWidth))); } // prettier-ignore setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore isContentActive = (): boolean | undefined => this._isContentActive; @@ -719,7 +688,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @observable _aiWinHeight = 88; - private _tagsBtnHeight = 22; + TagsBtnHeight = 22; @computed get currentScale() { const viewXfScale = this._props.DocumentView!().screenToLocalScale(); const x = NumCast(this.Document._height) / viewXfScale / 80; @@ -733,20 +702,68 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document */ @computed get viewScaling() { return 1 / this.currentScale; } // prettier-ignore /** - * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. + * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its nominal pixel size. */ - @computed get maxWidgetSize() { return Math.min(this._tagsBtnHeight * this.viewScaling, 0.25 * Math.min(NumCast(this.Document._width), NumCast(this.Document._height))); } // prettier-ignore + @computed get maxWidgetSize() { return Math.min(this.TagsBtnHeight * this.viewScaling, 0.25 * Math.min(NumCast(this.Document._width), NumCast(this.Document._height))); } // prettier-ignore /** * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content */ - @computed get uiBtnScaling() { return Math.max(this.maxWidgetSize / this._tagsBtnHeight, 1) * Math.min(1, this.viewScaling); } // prettier-ignore + @computed get uiBtnScaling() { return Math.max(this.maxWidgetSize / this.TagsBtnHeight, 1) * Math.min(1, this.viewScaling); } // prettier-ignore aiContentsWidth = () => (this.aiContentsHeight() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1); aiContentsHeight = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight * this.uiBtnScaling); + aiEditor = () => ( + <> + <div + className="documentView-editorView-history" + ref={r => this.historyRef(this._oldAiWheel, (this._oldAiWheel = r))} + style={{ + transform: `scale(${this.uiBtnScaling})`, + height: this.aiContentsHeight() / this.uiBtnScaling, + width: ((this._props.PanelWidth() - this.aiContentsWidth()) * 0.95) / this.uiBtnScaling, + }}> + {this._componentView?.componentAIViewHistory?.() ?? null} + </div> + <div + className="documentView-editorView" + style={{ + background: SnappingManager.userVariantColor, + width: `${100 / this.uiBtnScaling}%`, // + transform: `scale(${this.uiBtnScaling})`, + }} + ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}> + <div className="documentView-editorView-resizer" /> + {this._componentView?.componentAIView?.() ?? null} + {this._props.DocumentView?.() ? <TagsView background={this.backgroundBoxColor} Views={[this._props.DocumentView?.()]} /> : null} + </div> + </> + ); + @computed get tagsOverlay() { + return ( + <div + className="documentView-noAiWidgets" + style={{ + width: `${100 / this.uiBtnScaling}%`, // + transform: `scale(${this.uiBtnScaling})`, + height: Number.isNaN(this.maxWidgetSize) ? undefined : this.TagsBtnHeight * this.uiBtnScaling, + }}> + {this._props.DocumentView?.() && !this._props.docViewPath().slice(-2)[0].ComponentView?.isUnstyledView?.() ? <TagsView background={this.backgroundBoxColor} Views={[this._props.DocumentView?.()]} /> : null} + </div> + ); + } + tagsOverlayFunc = () => (this._props.DocumentView?.().showTags ? this.tagsOverlay : null); + @computed get widgetOverlay() { + return ( + <div className="documentView-widgetDecorations" style={{ transform: `scale(${this.uiBtnScaling})` }}> + {this.widgetDecorations} + </div> + ); + } + widgetOverlayFunc = () => (this.widgetDecorations ? this.widgetOverlay : null); @computed get viewBoxContents() { TraceMobx(); const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString; - const noBackground = this.Document.$isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); + const noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); return ( <> <div @@ -758,7 +775,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }}> <DocumentContentsView {...this._props} - layoutFieldKey={StrCast(this._renderDoc.layout_fieldKey, 'layout')} + layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')} pointerEvents={this.contentPointerEvents} setContentViewBox={this.setContentView} childFilters={this.childFilters} @@ -773,43 +790,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)} /> </div> - {!this._props.showAIEditor ? ( - <div - className="documentView-noAiWidgets" - style={{ - width: `${100 / this.uiBtnScaling}%`, // - transform: `scale(${this.uiBtnScaling})`, - bottom: Number.isNaN(this.maxWidgetSize) ? undefined : this.maxWidgetSize, - }}> - {this._props.DocumentView?.() && !this._props.docViewPath().slice(-2)[0].ComponentView?.isUnstyledView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null} - </div> - ) : ( - <> - <div - className="documentView-editorView-history" - ref={r => this.historyRef(this._oldAiWheel, (this._oldAiWheel = r))} - style={{ - transform: `scale(${this.uiBtnScaling})`, - height: this.aiContentsHeight() / this.uiBtnScaling, - width: ((this._props.PanelWidth() - this.aiContentsWidth()) * 0.95) / this.uiBtnScaling, - }}> - {this._componentView?.componentAIViewHistory?.() ?? null} - </div> - <div - className="documentView-editorView" - style={{ - background: SnappingManager.userVariantColor, - width: `${100 / this.uiBtnScaling}%`, // - transform: `scale(${this.uiBtnScaling})`, - }} - ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}> - <div className="documentView-editorView-resizer" /> - {this._componentView?.componentAIView?.() ?? null} - {this._props.DocumentView?.() ? <TagsView Views={[this._props.DocumentView?.()]} /> : null} - </div> - </> - )} - {this.widgetDecorations ?? null} + {this._props.showAIEditor ? this.aiEditor() : this.tagsOverlayFunc()} + {this.widgetOverlayFunc()} </> ); } @@ -957,7 +939,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document : (this.docContents ?? ( <div className="documentView-node" - id={this.Document.$type !== DocumentType.LINK ? this._docView?.DocUniqueId : undefined} + id={this.Document.type !== DocumentType.LINK ? this._docView?.DocUniqueId : undefined} style={{ ...style, background: this.backgroundBoxColor, @@ -1015,7 +997,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document borderRadius: this._componentView?.isUnstyledView?.() ? undefined : this.borderRounding, pointerEvents: this._pointerEvents === 'visiblePainted' ? 'none' : this._pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here) }}> - {this._componentView?.isUnstyledView?.() || this.Document.$type === DocumentType.CONFIG || !renderDoc ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation], this.Document)} + {this._componentView?.isUnstyledView?.() || this.Document.type === DocumentType.CONFIG || !renderDoc ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation], this.Document)} {jsx} </div> ); @@ -1194,6 +1176,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { @observable private _selected = false; @observable public static CurrentlyPlaying: DocumentView[] = []; // audio or video media views that are currently playing @observable public TagPanelHeight = 0; + @observable public TagPanelEditing = false; @computed get showTags() { return this.Document._layout_showTags || this._props.showTags; @@ -1210,7 +1193,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } @computed private get nativeScaling() { if (this.shouldNotScale) return 1; - const minTextScale = this.Document.$type === DocumentType.RTF ? 0.1 : 0; + const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; const ai = this._showAIEditor && this.nativeWidth === this.layoutDoc.width ? 95 : 0; const effNW = Math.max(this.effectiveNativeWidth - ai, 1); const effNH = Math.max(this.effectiveNativeHeight - ai, 1); @@ -1271,6 +1254,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public get ContentDiv() { return this._docViewInternal?._contentDiv; } // prettier-ignore public get ComponentView() { return this._docViewInternal?._componentView; } // prettier-ignore public get allLinks() { return this._docViewInternal?._allLinks ?? []; } // prettier-ignore + public get TagBtnHeight() { return this._docViewInternal?.TagsBtnHeight; } // prettier-ignore + public get UIBtnScaling() { return this._docViewInternal?.uiBtnScaling; } // prettier-ignore get LayoutFieldKey() { return Doc.LayoutFieldKey(this.Document, this._props.LayoutTemplateString); @@ -1325,7 +1310,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public startDragging = (x: number, y: number, dropAction: dropActionType | undefined, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource); public showContextMenu = (pageX: number, pageY: number) => this._docViewInternal?.onContextMenu(undefined, pageX, pageY); - public toggleNativeDimensions = () => this._docViewInternal && this.Document.$type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.NativeDimScaling() ?? 1, this._props.PanelWidth(), this._props.PanelHeight()); + public toggleNativeDimensions = () => this._docViewInternal && this.Document.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.NativeDimScaling() ?? 1, this._props.PanelWidth(), this._props.PanelHeight()); public iconify(finished?: () => void, animateTime?: number) { this.ComponentView?.updateIcon?.(); @@ -1348,7 +1333,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } public playAnnotation = () => { - const audioAnnoState = this.dataDoc.audioAnnoState ?? AudioAnnoState.stopped; + const audioAnnoState = this.Document._audioAnnoState ?? AudioAnnoState.stopped; const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null); const anno = audioAnnos?.lastElement(); if (anno instanceof AudioField) { @@ -1360,13 +1345,13 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { autoplay: true, loop: false, volume: 0.5, - onend: action(() => { this.dataDoc.audioAnnoState = AudioAnnoState.stopped; }), // prettier-ignore + onend: action(() => { this.Document._audioAnnoState = AudioAnnoState.stopped; }), // prettier-ignore }); - this.dataDoc.audioAnnoState = AudioAnnoState.playing; + this.Document._audioAnnoState = AudioAnnoState.playing; break; case AudioAnnoState.playing: (this.dataDoc[AudioPlay] as Howl)?.stop(); - this.dataDoc.audioAnnoState = AudioAnnoState.stopped; + this.Document._audioAnnoState = AudioAnnoState.stopped; break; default: } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index f82e980f5..ad6d93d43 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -48,11 +48,9 @@ export type StyleProviderFuncType = ( export interface FieldViewSharedProps { Document: Doc; TemplateDataDocument?: Doc; - LayoutTemplateString?: string; - LayoutTemplate?: () => Opt<Doc>; renderDepth: number; scriptContext?: unknown; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document - screenXPadding?: () => number; // padding in screen space coordinates (used by text box to reflow around UI buttons in carouselView) + screenXPadding?: (view: DocumentView | undefined) => number; // padding in screen space coordinates (used by text box to reflow around UI buttons in carouselView) xPadding?: number; yPadding?: number; dontRegisterView?: boolean; @@ -62,7 +60,8 @@ export interface FieldViewSharedProps { ignoreAutoHeight?: boolean; disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. hideClickBehaviors?: boolean; // whether to suppress menu item options for changing click behaviors - ignoreUsePath?: boolean; // ignore the usePath field for selecting the fieldKey (eg., on text docs) + suppressSetHeight?: boolean; + dontCenter?: 'x' | 'y' | 'xy' | undefined; LocalRotation?: () => number | undefined; // amount of rotation applied to freeformdocumentview containing document view containerViewPath?: () => DocumentView[]; fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document @@ -80,13 +79,12 @@ export interface FieldViewSharedProps { styleProvider: Opt<StyleProviderFuncType>; setTitleFocus?: () => void; focus: FocusFuncType; - onClickScript?: () => ScriptField; + onClickScript?: () => ScriptField | undefined; onDoubleClickScript?: () => ScriptField; onPointerDownScript?: () => ScriptField; onPointerUpScript?: () => ScriptField; onKey?: (e: KeyboardEvent, textBox: FormattedTextBox) => boolean | undefined; fitWidth?: (doc: Doc) => boolean | undefined; - dontCenter?: 'x' | 'y' | 'xy' | undefined; searchFilterDocs: () => Doc[]; showTitle?: () => string; whenChildContentsActiveChanged: (isActive: boolean) => void; @@ -102,7 +100,6 @@ export interface FieldViewSharedProps { waitForDoubleClickToClick?: () => 'never' | 'always' | undefined; defaultDoubleClick?: () => 'default' | 'ignore' | undefined; pointerEvents?: () => Opt<Property.PointerEvents>; - suppressSetHeight?: boolean; } /** @@ -110,13 +107,13 @@ export interface FieldViewSharedProps { * */ export interface FieldViewProps extends FieldViewSharedProps { DocumentView?: () => DocumentView; - fieldKey: string; isSelected: () => boolean; select: (ctrlPressed: boolean, shiftPress?: boolean) => void; docViewPath: () => DocumentView[]; setHeight?: (height: number) => void; NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal isHovering?: () => boolean; + fieldKey: string; // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React) // See currentUserUtils headerTemplate for examples of creating text boxes from html which set some of these fields diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index f699568f1..3190757e2 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { ClientUtils, DashColor, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; +import { ClientUtils, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; import { ScriptField } from '../../../../fields/ScriptField'; @@ -134,7 +134,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { min={NumCast(this.dataDoc.numBtnMin, 0)} max={NumCast(this.dataDoc.numBtnMax, 100)} number={checkResult} - size={Size.XSMALL} + size={Size.XXSMALL} setNumber={undoable(value => numScript(value), `${this.Document.title} button set from list`)} fillWidth /> @@ -252,7 +252,9 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { * Color button */ @computed get colorButton() { - const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; + const color = SnappingManager.userColor; + const pickedColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; + const background = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string; const curColor = (this.colorScript?.script.run({ this: this.Document, value: undefined, _readOnly_: true }).result as string) ?? 'transparent'; const tooltip: string = StrCast(this.Document.toolTip); @@ -270,10 +272,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { }} defaultPickerType="Classic" selectedColor={curColor} - type={Type.PRIM} + type={Type.TERT} color={color} - background={SnappingManager.userBackgroundColor} - icon={this.Icon(color) ?? undefined} + background={background} + icon={this.Icon(pickedColor) ?? undefined} tooltip={tooltip} label={this.label} /> @@ -287,19 +289,20 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const toggleStatus = script?.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean; const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; + const background = this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; const items = DocListCast(this.dataDoc.data); const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType)); return ( <MultiToggle tooltip={`Click to Toggle ${tooltip} or select new option`} - type={Type.PRIM} + type={Type.TERT} color={color} - background={undefined} + background={background} multiSelect={true} onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))} - isToggle={false} toggleStatus={toggleStatus} + showUntilToggle={BoolCast(this.Document.showUntilToggle)} label={selectedItems.length === 1 ? selectedItems[0] : this.label} items={items.map(item => ({ icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />, @@ -339,11 +342,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { <Toggle tooltip={`Toggle ${tooltip}`} toggleType={ToggleType.BUTTON} - type={inkShapeHack ? Type.TERT : Type.PRIM} + type={Type.TERT} toggleStatus={toggleStatus} text={buttonText} color={color} - background={inkShapeHack ? DashColor(SnappingManager.userBackgroundColor).darken(0.05).toString() : undefined} + triState={inkShapeHack} + background={color} icon={this.Icon(color)!} label={this.label} onPointerDown={e => @@ -404,12 +408,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { case ButtonType.ColorButton: return this.colorButton; case ButtonType.MultiToggleButton: return this.multiToggleButton; case ButtonType.ToggleButton: return this.toggleButton; - case ButtonType.ClickButton:return <IconButton {...btnProps} size={Size.MEDIUM} color={color} />; - case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} />; - case ButtonType.TextButton: return <Button {...btnProps} color={color} - background={SnappingManager.userBackgroundColor} text={StrCast(this.dataDoc.buttonText)}/>; - case ButtonType.MenuButton: return <IconButton {...btnProps} color={color} - background={SnappingManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; + case ButtonType.ClickButton: return <IconButton {...btnProps} size={Size.MEDIUM} color={color} background={color} />; + case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} background={color} />; + case ButtonType.TextButton: return <Button {...btnProps} color={color} background={color} + text={StrCast(this.dataDoc.buttonText)}/>; + case ButtonType.MenuButton: return <IconButton size={Size.LARGE} {...btnProps} color={color} background={color} + tooltipPlacement='right' onClick={scriptFunc} />; default: } return this.defaultButton; diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss index 202b0c701..d6cf95958 100644 --- a/src/client/views/nodes/IconTagBox.scss +++ b/src/client/views/nodes/IconTagBox.scss @@ -4,7 +4,6 @@ display: flex; position: relative; pointer-events: none; - background-color: rgb(218, 218, 218); border-radius: 50px; align-items: center; gap: 5px; diff --git a/src/client/views/nodes/IconTagBox.tsx b/src/client/views/nodes/IconTagBox.tsx index ddabd61e1..e3924eca7 100644 --- a/src/client/views/nodes/IconTagBox.tsx +++ b/src/client/views/nodes/IconTagBox.tsx @@ -1,22 +1,23 @@ -import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; -import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; -import { emptyFunction } from '../../../Utils'; -import { Doc } from '../../../fields/Doc'; +import { Doc, StrListCast } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; +import { AudioAnnoState } from '../../../server/SharedMediaTypes'; import { undoable } from '../../util/UndoManager'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { TagItem } from '../TagsView'; import { DocumentView } from './DocumentView'; import './IconTagBox.scss'; +import { Size, Toggle, ToggleType, Type } from '@dash/components'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { StyleProp } from '../StyleProp'; export interface IconTagProps { Views: DocumentView[]; - IsEditing: boolean; + IsEditing: boolean | undefined; } /** @@ -52,13 +53,46 @@ export class IconTagBox extends ObservableReactComponent<IconTagProps> { * @param key metadata icon button * @returns an icon for the metdata button */ - getButtonIcon = (doc: Doc, key: Doc): JSX.Element => { + getButtonIcon = (dv: DocumentView, key: Doc): JSX.Element => { const icon = StrCast(key.icon) as IconProp; const tag = StrCast(key.toolType); - const isActive = TagItem.docHasTag(doc, tag); - const color = isActive ? '#4476f7' : '#323232'; // TODO should use theme colors + const color = dv._props.styleProvider?.(dv.layoutDoc, dv.ComponentView?._props, StyleProp.FontColor) as string; + return ( + <Toggle + tooltip={`Click to add/remove the tag ${tag}`} + toggleStatus={TagItem.docHasTag(dv.Document, tag)} + toggleType={ToggleType.BUTTON} + icon={<FontAwesomeIcon className={`fontIconBox-icon-${ToggleType.BUTTON}`} icon={icon} color={color} />} + size={Size.XSMALL} + type={Type.PRIM} + onClick={() => this.setIconTag(tag, !TagItem.docHasTag(this.View.Document, tag))} + color={color} + /> + ); + }; - return <FontAwesomeIcon icon={icon} style={{ color, height: '20px', width: '20px' }} />; + /** + * Displays a button to play audio annotations on the document. + * NOTE: This should be generalized -- audio should + * @returns + */ + renderAudioButtons = (dv: DocumentView, anno: string) => { + const fcolor = dv._props.styleProvider?.(dv.layoutDoc, dv.ComponentView?._props, StyleProp.FontColor) as string; + const audioIconColors: { [key: string]: string } = { playing: 'green', stopped: fcolor ?? 'blue', recording: 'red' }; + const audioAnnoState = (audioDoc: Doc) => StrCast(audioDoc.audioAnnoState, AudioAnnoState.stopped); + const color = audioIconColors[audioAnnoState(this.View.Document)]; + return ( + <Toggle + tooltip={`click to play:${anno}`} + toggleStatus={true} + toggleType={ToggleType.BUTTON} + icon={<FontAwesomeIcon className={`fontIconBox-icon-${ToggleType.BUTTON}`} icon="file-audio" color={color} />} + size={Size.XSMALL} + type={Type.PRIM} + onClick={() => this.View?.playAnnotation()} + color={color} + /> + ); }; /** @@ -69,22 +103,15 @@ export class IconTagBox extends ObservableReactComponent<IconTagProps> { .map(key => ({ key, tag: StrCast(key.toolType) })) .filter(({ tag }) => this._props.IsEditing || TagItem.docHasTag(this.View.Document, tag) || (DocumentView.Selected().length === 1 && this.View.IsSelected)) .map(({ key, tag }) => ( - <Tooltip key={tag} title={<div className="dash-tooltip">Click to add/remove this card from the {tag} group</div>}> - <button - type="button" - onPointerDown={e => - setupMoveUpEvents(this, e, returnFalse, emptyFunction, clickEv => { - this.setIconTag(tag, !TagItem.docHasTag(this.View.Document, tag)); - clickEv.stopPropagation(); - }) - }> - {this.getButtonIcon(this.View.Document, key)} - </button> + <Tooltip key={tag} title={<div className="dash-tooltip">Click to add/remove the {tag} tag</div>}> + {this.getButtonIcon(this.View, key)} </Tooltip> )); // prettier-ignore - return !buttons.length ? null : ( + const audioannos = StrListCast(this.View.Document[Doc.LayoutFieldKey(this.View.Document) + '_audioAnnotations_text']); + return !buttons.length && !audioannos.length ? null : ( <div className="card-button-container" style={{ fontSize: '50px' }}> + {audioannos.length ? this.renderAudioButtons(this.View, audioannos.lastElement()) : null} {buttons} </div> ); diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 3d6942e6f..4a6e8eb49 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -114,7 +114,6 @@ width: 100%; height: 100%; position: absolute; - background: black; display: flex; flex-direction: row; align-items: center; @@ -143,11 +142,8 @@ } } .imageBox-regenerateDropTarget { - right: 30; - border-radius: 50%; - svg { - border-radius: 50%; - } + right: 35; + transform-origin: 70px 35px; } .imageBox-fader img { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 3b3bc808a..2b9a78596 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -45,8 +45,9 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; -import { RichTextField } from '../../../fields/RichTextField'; +import { ComputedField } from '../../../fields/ScriptField'; +const DefaultPath = '/assets/unknown-file-icon-hi.png'; export class ImageEditorData { // eslint-disable-next-line no-use-before-define private static _instance: ImageEditorData; @@ -146,11 +147,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }, { fireImmediately: true, delay: 1000 } ); - const { layoutDoc } = this; this._disposers.path = reaction( () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }), ({ nativeSize, width, height }) => { - if ((layoutDoc === this.layoutDoc && !this.layoutDoc._layout_nativeDimEditable) || !height) { + if (!this.layoutDoc._layout_nativeDimEditable || !height) { this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; } }, @@ -210,15 +210,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { if (de.metaKey || hitDropTarget(e.target as HTMLElement, this._overlayIconRef.current)) { added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; + this.Document.$backgroundColor_alternate = ComputedField.MakeFunction('this.data_alternates[0]?.$backgroundColor'); return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop); }, true); } else if (hitDropTarget(e.target as HTMLElement, this._regenerateIconRef.current)) { this._regenerateLoading = true; const drag = de.complete.docDragData.draggedDocuments.lastElement(); const dragField = drag[Doc.LayoutFieldKey(drag)]; + const descText = RTFCast(dragField)?.Text || StrCast(dragField) || RTFCast(drag.text)?.Text || StrCast(drag.text) || StrCast(this.Document.title); const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title)); const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text); - DrawingFillHandler.drawingToImage(this.Document, 100, newPrompt(dragField instanceof RichTextField ? dragField.Text : ''), drag)?.then(action(() => (this._regenerateLoading = false))); + DrawingFillHandler.drawingToImage(this.Document, 90, newPrompt(descText), drag)?.then(action(() => (this._regenerateLoading = false))); added = false; } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; @@ -252,12 +254,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const nw = nscale / oldnativeWidth; this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw; this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw; - this.dataDoc._freeform_panX = nw * NumCast(this.dataDoc._freeform_panX); - this.dataDoc._freeform_panY = nw * NumCast(this.dataDoc._freeform_panY); - this.dataDoc._freeform_panX_max = this.dataDoc._freeform_panX_max ? nw * NumCast(this.dataDoc._freeform_panX_max) : undefined; - this.dataDoc._freeform_panX_min = this.dataDoc._freeform_panX_min ? nw * NumCast(this.dataDoc._freeform_panX_min) : undefined; - this.dataDoc._freeform_panY_max = this.dataDoc._freeform_panY_max ? nw * NumCast(this.dataDoc._freeform_panY_max) : undefined; - this.dataDoc._freeform_panY_min = this.dataDoc._freeform_panY_min ? nw * NumCast(this.dataDoc._freeform_panY_min) : undefined; + this.dataDoc.freeform_panX = nw * NumCast(this.dataDoc.freeform_panX); + this.dataDoc.freeform_panY = nw * NumCast(this.dataDoc.freeform_panY); + this.dataDoc.freeform_panX_max = this.dataDoc.freeform_panX_max ? nw * NumCast(this.dataDoc.freeform_panX_max) : undefined; + this.dataDoc.freeform_panX_min = this.dataDoc.freeform_panX_min ? nw * NumCast(this.dataDoc.freeform_panX_min) : undefined; + this.dataDoc.freeform_panY_max = this.dataDoc.freeform_panY_max ? nw * NumCast(this.dataDoc.freeform_panY_max) : undefined; + this.dataDoc.freeform_panY_min = this.dataDoc.freeform_panY_min ? nw * NumCast(this.dataDoc.freeform_panY_min) : undefined; const newnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); DocListCast(this.dataDoc[this.annotationKey]).forEach(doc => { doc.x = (NumCast(doc.x) / oldnativeWidth) * newnativeWidth; @@ -293,12 +295,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const anchw = NumCast(cropping._width); const anchh = NumCast(cropping._height); const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) / anchh; - cropping.title = 'crop: ' + this.Document.title; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); + cropping.onClick = undefined; cropping._width = anchw * (this._props.NativeDimScaling?.() || 1); cropping._height = anchh * (this._props.NativeDimScaling?.() || 1); - cropping.onClick = undefined; + cropping.$title = 'crop: ' + this.Document.title; cropping.$annotationOn = undefined; cropping.$isDataDoc = true; cropping.$backgroundColor = undefined; @@ -309,7 +311,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { cropping.$data_nativeWidth = anchw; cropping.$data_nativeHeight = anchh; cropping.$freeform_scale = viewScale; - cropping.$reeform_panX = anchx / viewScale; + cropping.$freeform_panX = anchx / viewScale; cropping.$freeform_panY = anchy / viewScale; cropping.$freeform_scale_min = viewScale; cropping.$freeform_panX_min = anchx / viewScale; @@ -399,13 +401,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; // updateIcon = () => new Promise<void>(res => res()); - updateIcon = (/* usePanelDimensions?: boolean */) => { - const contentDiv = this._mainCont; - return !contentDiv + updateIcon = (/* usePanelDimensions?: boolean */) => + !this._mainCont || !DocListCast(this.dataDoc[this.annotationKey]).length ? new Promise<void>(res => res()) : UpdateIcon( this.layoutDoc[Id] + '_icon_' + new Date().getTime(), - contentDiv, + this._mainCont, this._props.PanelWidth(), // usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), this._props.PanelHeight(), // usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), this._props.PanelWidth(), @@ -420,14 +421,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.dataDoc.icon_nativeHeight = nativeHeight; } ); - }; choosePath = (url: URL) => { if (!url?.href) return ''; const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return ClientUtils.CorsProxy(url.href); - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith('/assets/unknown-file-icon-hi.png')) return `/assets/unknown-file-icon-hi.png`; + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith(DefaultPath)) return DefaultPath; const ext = extname(url.href); return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); @@ -441,7 +441,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @computed get nativeSize() { TraceMobx(); - if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; + if (this.paths.length && this.paths[0].includes(DefaultPath)) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)); const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)); const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1); @@ -451,15 +451,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { /** * How much the content of the view is being scaled based on its nesting and its fit-to-width settings */ - @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale * ( this._props.NativeDimScaling?.() || 1); } // prettier-ignore + @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.()??1); } // prettier-ignore /** * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. */ - @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.5 * Math.min(NumCast(this.Document.width)))* this.viewScaling; } // prettier-ignore + @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.2 * this._props.PanelWidth())*this.viewScaling; } // prettier-ignore /** * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content */ - @computed get uiBtnScaling() { return Math.min(this.maxWidgetSize / this._sideBtnWidth, 1); } // prettier-ignore + @computed get uiBtnScaling() { return Math.min(1/(this._props.NativeDimScaling?.()??1), this.maxWidgetSize / this._sideBtnWidth); } // prettier-ignore @computed get overlayImageIcon() { const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`]; @@ -506,27 +506,30 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } @computed get regenerateImageIcon() { return ( - <div - className="imageBox-regenerateDropTarget" - ref={this._regenerateIconRef} - onClick={() => DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs), { openLocation: OpenWhere.addRight })} - style={{ - display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', - transform: `scale(${this.uiBtnScaling})`, - width: this._sideBtnWidth, - height: this._sideBtnWidth, - background: 'transparent', - // color: SettingsManager.userBackgroundColor, - }}> - {this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width="100%" height="100%" /> : <FontAwesomeIcon icon="portrait" color={SettingsManager.userColor} size="lg" />} - </div> + <Tooltip title={'click to show AI generations. Drop an image on to create a new generation'}> + <div + className="imageBox-regenerateDropTarget" + ref={this._regenerateIconRef} + onClick={() => DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs), { openLocation: OpenWhere.addRight })} + style={{ + display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', + transform: `scale(${this.uiBtnScaling})`, + width: this._sideBtnWidth, + height: this._sideBtnWidth, + background: 'black', + color: 'white', + // color: SettingsManager.userBackgroundColor, + }}> + {this._regenerateLoading ? <ReactLoading type="spin" width="100%" height="100%" /> : <FontAwesomeIcon icon="portrait" size="lg" />} + </div> + </Tooltip> ); } @computed get paths() { const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); // retrieve the primary image URL that is being rendered from the data doc const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images - const defaultUrl = new URL(ClientUtils.prepend('/assets/unknown-file-icon-hi.png')); + const defaultUrl = new URL(ClientUtils.prepend(DefaultPath)); const altpaths = alts ?.map(doc => (doc instanceof Doc ? (ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl) : defaultUrl)) @@ -539,11 +542,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @computed get content() { TraceMobx(); + const usePath = StrCast(this.Document[this.fieldKey + '_usePath']); + const alternate = '_' + usePath.replace(':hover', ''); + const altColor = DashColor(StrCast(this.Document[this.fieldKey + alternate], StrCast(this.Document['$backgroundColor' + alternate], 'black'))); + const backColor = DashColor((this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string) ?? Colors.WHITE); // allow use case where the image is transparent when the alpha value is to smallest possible value from UI (alpha = 1 out of 255) const backAlpha = backColor.alpha() < 0.015 && backColor.alpha() > 0 ? backColor.alpha() : 1; - const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0]; - const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); + const fadepath = this.layoutDoc.hideImage ? '' : this.paths[0]; + const srcpath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); const { nativeWidth, nativeHeight /* , nativeOrientation */ } = this.nativeSize; const rotation = NumCast(this.dataDoc[this.fieldKey + '_rotation']); const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1; @@ -577,14 +584,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { key="paths" src={srcpath} style={{ transform, transformOrigin }} - onError={action(e => { - this._error = e.toString(); - })} + onError={action(e => (this._error = e.toString()))} draggable={false} width={nativeWidth} /> {fadepath === srcpath ? null : ( - <div className={`imageBox-fadeBlocker${this.usingAlternate ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}> + <div + className={`imageBox-fadeBlocker${!this.usingAlternate ? '-hover' : ''}`} + style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms'), background: altColor.alpha() === 0 ? 'transparent' : altColor.toString() }}> <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> </div> )} @@ -648,7 +655,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { onClick={action(async () => { this._regenerateLoading = true; if (this._fireflyRefStrength) { - DrawingFillHandler.drawingToImage(this.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title), this.Document)?.then(action(() => (this._regenerateLoading = false))); + DrawingFillHandler.drawingToImage(this.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title))?.then(action(() => (this._regenerateLoading = false))); } else { SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then( action(newImgs => { @@ -702,21 +709,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale); marqueeDown = (e: React.PointerEvent) => { - if (!this.dataDoc[this.fieldKey]) { - this.chooseImage(); - } else if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { - setupMoveUpEvents( - this, - e, - action(moveEv => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); - return true; - }), - returnFalse, - () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), - false - ); + if (!e.altKey && e.button === 0) { + if (NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { + setupMoveUpEvents( + this, + e, + action(moveEv => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); + return true; + }), + returnFalse, + () => { + if (!this.dataDoc[this.fieldKey]) this.chooseImage(); + else MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + }, + false + ); + } } }; @action @@ -741,8 +751,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { TraceMobx(); const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string; const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad; - const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); - const doc = this.usingAlternate ? (alts.lastElement() ?? this.Document) : this.Document; return ( <div className="imageBox" @@ -767,7 +775,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <CollectionFreeFormView ref={this._ffref} {...this._props} - Document={doc} + Document={this.Document} setContentViewBox={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 8911fac6d..be897b3f3 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -9,7 +9,7 @@ import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { DocCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { DocumentType } from '../../documents/DocumentTypes'; -import { Docs } from '../../documents/Documents'; +import { Docs, DocumentOptions } from '../../documents/Documents'; import { SetupDrag } from '../../util/DragManager'; import { CompiledScript } from '../../util/Scripting'; import { undoable } from '../../util/UndoManager'; @@ -22,6 +22,7 @@ import './KeyValueBox.scss'; import { KeyValuePair } from './KeyValuePair'; import { OpenWhere } from './OpenWhere'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { DocLayout } from '../../../fields/DocSymbols'; export type KVPScript = { script: CompiledScript; @@ -91,10 +92,11 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { return !script.compiled ? undefined : { script, type, onDelegate }; }; - public static ApplyKVPScript = (doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => { + public static ApplyKVPScript = (doc: Doc, keyIn: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => { const { script, type, onDelegate } = kvpScript; - // const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates - const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc); + const chooseDelegate = forceOnDelegate || onDelegate || keyIn.startsWith('_'); + const key = chooseDelegate && keyIn.startsWith('$') ? keyIn.slice(1) : keyIn; + const target = chooseDelegate ? doc : DocCast(doc.proto, doc); let field: FieldType | undefined; switch (type) { case 'computed': field = new ComputedField(script); break; // prettier-ignore @@ -121,10 +123,16 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { return false; }; - public static SetField = undoable((doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => { + public static SetField = undoable((doc: Doc, key: string, value: string, forceOnDelegateIn?: boolean, setResult?: (value: FieldResult) => void) => { const script = KeyValueBox.CompileKVPScript(value); if (!script) return false; - return KeyValueBox.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult); + const ldoc = key.startsWith('_') ? doc[DocLayout] : doc; + const forceOnDelegate = forceOnDelegateIn || (ldoc !== doc && !value.startsWith('=')); + // an '=' value redirects a key targeting the render template to the root document. + // also, if ldoc and doc are equal, allow redirecting to data document when not using an equal + // in either case, get rid of initial '_' which forces writing to layout + const setKey = value.startsWith('=') || ldoc === doc ? key.replace(/^_/, '') : key; + return KeyValueBox.ApplyKVPScript(doc, setKey, script, forceOnDelegate, setResult); }, 'Set Doc Field'); onPointerDown = (e: React.PointerEvent): void => { @@ -156,12 +164,16 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { }); }); - const layoutProtos = Doc.GetAllPrototypes(this.layoutDoc); + const docinfos = new DocumentOptions(); + + const layoutProtos = this.layoutDoc !== doc ? Doc.GetAllPrototypes(this.layoutDoc) : []; layoutProtos.forEach(proto => { Object.keys(proto) + .filter(key => !(key in ids) || docinfos['_' + key]) // if '_key' is in docinfos (as opposed to just 'key'), then the template Doc's value should be displayed instead of the root document's value .map(key => '_' + key) .forEach(key => { - if (!(key.replace(/^_/, '') in ids) && doc[key] !== ComputedField.undefined) { + if (doc[key] !== ComputedField.undefined) { + if (key.replace(/^_/, '') in ids) delete ids[key.replace(/^_/, '')]; ids[key] = key; } }); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 93f5231cb..c9e0aea5a 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -16,6 +16,7 @@ import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; import './KeyValueBox.scss'; import './KeyValuePair.scss'; import { OpenWhere } from './OpenWhere'; +import { DocLayout } from '../../../fields/DocSymbols'; // Represents one row in a key value plane @@ -60,31 +61,23 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { }; render() { - // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; - const layoutField = this._props.keyName.startsWith('_'); - let doc = layoutField ? Doc.Layout(this._props.doc) : this._props.doc; - let protoCount = doc !== this._props.doc && !layoutField ? 1 : 0; - while (doc) { - if (Object.keys(doc).includes(this._props.keyName.replace(/^_/, ''))) { - break; - } + let doc = this._props.keyName.startsWith('_') ? this._props.doc[DocLayout] : this._props.doc; + const layoutField = doc !== this._props.doc; + const key = layoutField ? this._props.keyName.replace(/^_/, '') : this._props.keyName; + let protoCount = 0; + while (doc && !Object.keys(doc).includes(key)) { protoCount++; doc = DocCast(doc.proto); } - const parenCount = Math.max(0, protoCount); - const keyStyle = protoCount === 0 && doc === this._props.doc ? 'black' : 'blue'; - + const parenCount = Math.max(0, protoCount - 1); + const keyStyle = layoutField ? 'red' : protoCount === 0 ? 'black' : 'blue'; const hover = { transition: '0.3s ease opacity', opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; - + const docOpts = Object.entries(new DocumentOptions()); return ( <tr - className={this._props.rowStyle} - onPointerEnter={action(() => { - this.isPointerOver = true; - })} - onPointerLeave={action(() => { - this.isPointerOver = false; - })}> + className={this._props.rowStyle} // + onPointerEnter={action(() => (this.isPointerOver = true))} + onPointerLeave={action(() => (this.isPointerOver = false))}> <td className="keyValuePair-td-key" style={{ width: `${this._props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> <button @@ -92,17 +85,16 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { style={hover} className="keyValuePair-td-key-delete" onClick={undoable(() => { - if (Object.keys(this._props.doc).indexOf(this._props.keyName) !== -1) { - delete this._props.doc[this._props.keyName]; - } else delete DocCast(this._props.doc.proto)?.[this._props.keyName]; + delete (Object.keys(doc).indexOf(key) !== -1 ? doc : DocCast(this._props.doc.proto))[key]; }, 'set key value')}> X </button> <input className="keyValuePair-td-key-check" type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} /> - <Tooltip title={Object.entries(new DocumentOptions()).find((pair: [string, FInfo]) => pair[0].replace(/^_/, '') === this._props.keyName)?.[1].description ?? ''}> - <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (this._props.keyName.replace(/__/g, '').replace(/^_/, '').match(/_/g)?.length || 0), color: keyStyle }}> - {(layoutField ? '_' : '$').repeat(parenCount)} - {(keyStyle === 'blue' && !layoutField && !parenCount ? '$' : '') + this._props.keyName} + <Tooltip title={(docOpts.find(([k]) => k.replace(/^_/, '') === key)?.[1] as FInfo)?.description ?? ''}> + <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (key.match(/_/g)?.length || 0), color: keyStyle }}> + {'('.repeat(parenCount)} + {(keyStyle === 'black' ? '' : layoutField ? '_' : '$') + key} + {')'.repeat(parenCount)} </div> </Tooltip> </div> @@ -133,7 +125,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { pinToPres: returnZero, }} GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)} - SetValue={(value: string) => Doc.SetField(this._props.doc, this._props.keyName, value)} + SetValue={value => Doc.SetField(this._props.doc, this._props.keyName, value)} /> </div> </td> diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index f2160feb7..eaea272dc 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -23,9 +23,9 @@ // glr: This should really be the same component as text and PDFs .pdfBox-sidebarBtn { background: global.$black; - height: 25px; - width: 25px; - right: 5px; + height: 20px; + width: 20px; + right: 0px; color: global.$white; display: flex; position: absolute; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 78ddafa88..36d260fb9 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -499,8 +499,10 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { title="Toggle Sidebar" style={{ display: !this._props.isContentActive() ? 'none' : undefined, - top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, + top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 0, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, + transformOrigin: 'top right', + transform: `scale(${this._props.DocumentView?.().UIBtnScaling || 1})`, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index f9de4ab5a..547a2efa8 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -141,8 +141,8 @@ audiotag:hover { position: absolute; top: 0; right: 0; - width: 17px; - height: 17px; + width: 20px; + height: 20px; font-size: 11px; border-radius: 3px; color: white; @@ -153,6 +153,7 @@ audiotag:hover { align-items: center; cursor: grabbing; box-shadow: global.$standard-box-shadow; + transform-origin: top right; // transition: 0.2s; opacity: 0.3; &:hover { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 5f132ecdf..a45b8a488 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -17,7 +17,7 @@ import { BsMarkdownFill } from 'react-icons/bs'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id, ToString } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -141,29 +141,29 @@ 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 fontStyle() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontStyle) as string; } // prettier-ignore - @computed get fontDecoration() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontDecoration) as string; } // prettier-ignore + @computed get fontColor() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore + @computed get fontSize() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore + @computed get fontFamily() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore + @computed get fontWeight() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore + @computed get fontStyle() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontStyle) as string; } // prettier-ignore + @computed get fontDecoration() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontDecoration) as string; } // prettier-ignore - set _recordingDictation(value) { + set recordingDictation(value) { !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined); } // eslint-disable-next-line no-return-assign @computed get config() { return FormattedTextBox.MakeConfig(this._rules = new RichTextRules(this.Document, this), this); } // prettier-ignore - @computed get _recordingDictation() { return this.dataDoc?.mediaState === mediaState.Recording; } // prettier-ignore - @computed get SidebarShown() { return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } // prettier-ignore + @computed get recordingDictation() { return this.dataDoc?.mediaState === mediaState.Recording; } // prettier-ignore + @computed get SidebarShown() { return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } // prettier-ignore @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.sidebarKey]); } // prettier-ignore @computed get noSidebar() { return this.DocumentView?.()._props.hideDecorationTitle || this._props.noSidebar || this.Document._layout_noSidebar; } // prettier-ignore - @computed get layout_sidebarWidthPercent() { return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore - @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4')); } // prettier-ignore + @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, this._showSidebar ? '20%' :'0%'); } // prettier-ignore + @computed get sidebarColor() { return StrCast(this.layoutDoc._sidebar_color, StrCast(this.layoutDoc["_"+this.fieldKey + '_backgroundColor'], '#e4e4e4')); } // prettier-ignore @computed get layout_autoHeight() { return (this._props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this._props.ignoreAutoHeight; } // prettier-ignore - @computed get textHeight() { return NumCast(this.dataDoc[this.fieldKey + '_height']); } // prettier-ignore - @computed get scrollHeight() { return NumCast(this.dataDoc[this.fieldKey + '_scrollHeight']); } // prettier-ignore - @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.dataDoc[this.sidebarKey + '_height']); } // prettier-ignore + @computed get textHeight() { return NumCast(this.layoutDoc["_"+this.fieldKey + '_height']); } // prettier-ignore + @computed get scrollHeight() { return NumCast(this.layoutDoc["_"+this.fieldKey + '_scrollHeight']); } // prettier-ignore + @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.layoutDoc["_"+this.sidebarKey + '_height']); } // prettier-ignore @computed get titleHeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number || 0; } // prettier-ignore @computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } // prettier-ignore @computed get sidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore @@ -215,7 +215,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const rootDoc: Doc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document); + const rootDoc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : this.rootDoc; if (!pinProps && this.EditorView?.state.selection.empty) return rootDoc; const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc }); this.addDocument(anchor); @@ -246,7 +246,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB anchor.followLinkAudio = true; let stopFunc: () => void = emptyFunction; target.$mediaState = mediaState.Recording; - DictationManager.recordAudioAnnotation(target[DocData], Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore + DictationManager.recordAudioAnnotation(target, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore const reactionDisposer = reaction( () => target.mediaState, @@ -308,7 +308,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB leafText = (node: Node) => { if (node.type === this.EditorView?.state.schema.nodes.dashField) { - const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); + const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); const fieldKey = StrCast(node.attrs.fieldKey); return ( (node.attrs.hideKey ? '' : fieldKey + ':') + // @@ -316,7 +316,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); } if (node.type === this.EditorView?.state.schema.nodes.dashDoc) { - const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); + const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); return refDoc[ToString](); } return ''; @@ -743,15 +743,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB @action toggleSidebar = (preview: boolean = false) => { const defaultSidebar = 250; - const prevWidth = 1 - this.sidebarWidth() / DivWidth(this._ref.current!); + const dw = DivWidth(this._ref.current); + const prevWidth = 1 - this.sidebarWidth() / dw / this.nativeScaling(); if (preview) this._showSidebar = true; else { - this.layoutDoc[this.sidebarKey + '_freeform_scale_max'] = 1; - this.layoutDoc._layout_showSidebar = - (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(defaultSidebar / (NumCast(this.layoutDoc._width) + defaultSidebar)) * 100}%` : '0%') !== '0%'; + this.layoutDoc._layout_sidebarWidthPercent = + this.sidebarWidthPercent === '0%' // + ? `${(defaultSidebar / (NumCast(this.layoutDoc._width) + defaultSidebar)) * 100}%` // + : '0%'; + this.layoutDoc._layout_showSidebar = this.sidebarWidthPercent !== '0%'; } - this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) + defaultSidebar : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); + this.layoutDoc._width = + !preview && this.SidebarShown // + ? NumCast(this.layoutDoc._width) + defaultSidebar + : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; sidebarDown = (e: React.PointerEvent) => { const batch = UndoManager.StartBatch('toggle sidebar'); @@ -769,7 +775,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { const localDelta = this.DocumentView?.().screenToViewTransform().transformDirection(delta[0], delta[1]) ?? delta; - const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100; + const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.sidebarWidthPercent.replace('%', ''))) / 100; const width = NumCast(this.layoutDoc._width) + localDelta[0]; this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%'; this.layoutDoc.width = width; @@ -1034,14 +1040,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; breakupDictation = () => { - if (this.EditorView && this._recordingDictation) { + if (this.EditorView && this.recordingDictation) { this.stopDictation(/* true */); this._break = true; const { state } = this.EditorView; const { to } = state.selection; const updated = TextSelection.create(state.doc, to, to); this.EditorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({}))); - if (this._recordingDictation) { + if (this.recordingDictation) { this.recordDictation(); } } @@ -1214,7 +1220,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // Since we also monitor all component height changes, this will update the document's height. resetNativeHeight = (scrollHeight: number) => { const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight); - this.dataDoc[this.fieldKey + '_height'] = scrollHeight; + this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight; if (nh) this.layoutDoc._nativeHeight = scrollHeight; }; @@ -1223,7 +1229,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } @computed get contentScaling() { - return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this._props.NativeDimScaling?.() || 1 : 1; + return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this.nativeScaling() : 1; } componentDidMount() { !this._props.dontSelectOnLoad && this._props.setContentViewBox?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. @@ -1333,14 +1339,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (!this._props.dontRegisterView) { this._disposers.record = reaction( - () => this._recordingDictation, + () => this.recordingDictation, () => { this.stopDictation(/* true */); - this._recordingDictation && this.recordDictation(); + this.recordingDictation && this.recordDictation(); }, { fireImmediately: true } ); - if (this._recordingDictation) setTimeout(this.recordDictation); + if (this.recordingDictation) setTimeout(this.recordDictation); } this._disposers.scroll = reaction( () => NumCast(this.layoutDoc._layout_scrollTop), @@ -1516,8 +1522,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._editorView.TextView = this; } - const selectOnLoad = - Doc.AreProtosEqual(this._props.TemplateDataDocument ?? DocCast(this.Document.rootDocument, this.Document), DocumentView.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())); + const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.rootDoc, DocumentView.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())); const selLoadChar = FormattedTextBox.SelectOnLoadChar; if (selectOnLoad) { DocumentView.SetSelectOnLoad(undefined); @@ -1560,8 +1565,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; componentWillUnmount() { - if (this._recordingDictation) { - this._recordingDictation = !this._recordingDictation; + if (this.recordingDictation) { + this.recordingDictation = !this.recordingDictation; } Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); @@ -1596,7 +1601,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }); } - if (this._recordingDictation && !e.ctrlKey && e.button === 0) { + if (this.recordingDictation && !e.ctrlKey && e.button === 0) { this.breakupDictation(); } FormattedTextBoxComment.textBox = this; @@ -1754,8 +1759,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB */ tryKeepingFocus = (target: Element | null) => { for (let newFocusEle = target instanceof HTMLElement ? target : null; newFocusEle; newFocusEle = newFocusEle?.parentElement) { - // test if parent of new focused element is a UI button (should be more specific than testing className) - if (newFocusEle?.className === 'fonticonbox' || newFocusEle?.className === 'popup-container') { + // bcz: HACK!! test if parent of new focused element is a UI button (should be more specific than testing className) + if (['fonticonbox', 'antimodeMenu-cont', 'popup-container'].includes(newFocusEle?.className ?? '')) { return this.EditorView?.focus(); // keep focus on text box } } @@ -1879,10 +1884,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation const setScrollHeight = () => { - this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight; + this.layoutDoc['_' + this.fieldKey + '_scrollHeight'] = scrollHeight; }; - if (this.Document === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { + if (this.Document === this.layoutDoc || this.layoutDoc.rootDocument) { setScrollHeight(); } else { setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... @@ -1891,7 +1896,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }; fitContentsToBox = () => BoolCast(this.Document._freeform_fitContentsToBox); - sidebarContentScaling = () => (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + nativeScaling = () => this._props.NativeDimScaling?.() || 1; sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.sidebarKey) => { if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); @@ -1899,17 +1904,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.sidebarKey); sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.sidebarKey); setSidebarHeight = (height: number) => { - this.dataDoc[this.sidebarKey + '_height'] = height; + this.layoutDoc['_' + this.sidebarKey + '_height'] = height; }; - sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); sidebarScreenToLocal = () => this._props .ScreenToLocalTransform() - .translate(-(this._props.PanelWidth() - this.sidebarWidth()) / (this._props.NativeDimScaling?.() || 1), 0) - .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this._props.NativeDimScaling?.() || 1)); + .translate(-(this._props.PanelWidth() - this.sidebarWidth()) / this.nativeScaling(), 0) + .scale(1 / this.nativeScaling()); @computed get audioHandle() { - return !this._recordingDictation ? null : ( + return !this.recordingDictation ? null : ( <div className="formattedTextBox-dictation" onPointerDown={e => @@ -1919,7 +1924,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB returnFalse, emptyFunction, action(() => { - this._recordingDictation = !this._recordingDictation; + this.recordingDictation = !this.recordingDictation; }) ) }> @@ -1927,6 +1932,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB </div> ); } + private _sideBtnWidth = 35; + /** + * How much the content of the view is being scaled based on its nesting and its fit-to-width settings + */ + @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale ; } // prettier-ignore + /** + * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. + */ + @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.2 * this._props.PanelWidth())*this.viewScaling; } // prettier-ignore + /** + * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content + */ + @computed get uiBtnScaling() { return this.maxWidgetSize / this._sideBtnWidth; } // prettier-ignore + @computed get sidebarHandle() { TraceMobx(); const annotated = DocListCast(this.dataDoc[this.sidebarKey]).filter(d => d?.author).length; @@ -1941,6 +1960,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB backgroundColor, color, opacity: annotated ? 1 : undefined, + transform: `scale(${this.uiBtnScaling})`, }}> <FontAwesomeIcon icon="comment-alt" /> </div> @@ -1985,7 +2005,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB isAnnotationOverlay={false} select={emptyFunction} isAnyChildContentActive={returnFalse} - NativeDimScaling={this.sidebarContentScaling} + NativeDimScaling={this.nativeScaling} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.sidebarRemDocument} moveDocument={this.sidebarMoveDocument} @@ -2002,7 +2022,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); }; return ( - <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> {renderComponent(StrCast(this.layoutDoc[this.sidebarKey + '_type_collection']))} </div> ); @@ -2050,7 +2070,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return this._fieldKey; } @computed get _fieldKey() { - const usePath = this._props.ignoreUsePath ? '' : StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]); + const usePath = StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]); return this._props.fieldKey + (usePath && (!usePath.includes(':hover') || this._props.isHovering?.() || this._props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : ''); } onPassiveWheel = (e: WheelEvent) => { @@ -2060,7 +2080,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) if (this._props.isContentActive()) { - const scale = this._props.NativeDimScaling?.() || 1; + const scale = this.nativeScaling(); const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > const height = Number(styleFromLayout.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable @@ -2077,15 +2097,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB render() { TraceMobx(); - const scale = this._props.NativeDimScaling?.() || 1; - const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; + const scale = this.nativeScaling(); + const rounded = StrCast(this.layoutDoc._layout_borderRounding) === '100%' ? '-rounded' : ''; setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); - const scrSize = (which: number, view = this._props.docViewPath().slice(-which)[0]) => - [view?._props.PanelWidth() /(view?.screenToLocalScale()??1), view?._props.PanelHeight() / (view?.screenToLocalScale()??1)]; // prettier-ignore - const scrMargin = [Math.max(0, (scrSize(2)[0] - scrSize(1)[0]) / 2), Math.max(0, (scrSize(2)[1] - scrSize(1)[1]) / 2)]; - const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), this._props.xPadding ?? 0, 0, ((this._props.screenXPadding?.() ?? 0) - scrMargin[0]) * this.ScreenToLocalBoxXf().Scale); - const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, ((this._props.yPadding ?? 0) - scrMargin[1]) * this.ScreenToLocalBoxXf().Scale); + const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), 0, this._props.xPadding ?? 0, this._props.screenXPadding?.(this._props.DocumentView?.()) ?? 0); + const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, this._props.yPadding ?? 0); // prettier-ignore const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > return this.isLabel ? ( <LabelBox {...this._props} /> @@ -2106,7 +2123,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB height: `${100 / scale}%`, }), transition: 'inherit', - paddingBottom: this.tagsHeight, // overflowY: this.layoutDoc._layout_autoHeight ? "hidden" : undefined, color: this.fontColor, fontSize: this.fontSize, @@ -2139,13 +2155,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._scrollRef = r; }} style={{ - width: this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, + width: this.noSidebar ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR || this.layoutDoc._layout_hideScroll ? 'hidden' : this.layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} onDrop={this.ondrop}> <div - className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered && this.scrollHeight <= (this._props.fitWidth?.(this.Document) ? this._props.PanelHeight() : NumCast(this.layoutDoc._height)) ? 'centered' : ''} ${this.layoutDoc.hCentering}`} + className={`formattedTextBox-inner${rounded} ${this.dataDoc.text_centered && this.scrollHeight <= (this._props.fitWidth?.(this.Document) ? this._props.PanelHeight() : NumCast(this.layoutDoc._height)) ? 'centered' : ''} ${this.layoutDoc.hCentering}`} ref={this.createDropTarget} style={{ padding: StrCast(this.layoutDoc._textBoxPadding), @@ -2159,7 +2175,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }} /> </div> - {this.noSidebar || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} + {this.noSidebar || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection} {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden || this.Document.quiz ? null : this.sidebarHandle} {this.audioHandle} {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null} diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index eabc6455f..7ae1fc202 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -50,8 +50,7 @@ export function buildKeymap<S extends Schema<string>>(schema: S, tbox?: Formatte const canEdit = (state: EditorState) => { if (!tbox) return true; - const permissions = GetEffectiveAcl(tbox._props.TemplateDataDocument ?? tbox.Document[DocData]); - switch (permissions) { + switch (GetEffectiveAcl(tbox.dataDoc)) { case AclAugment: { // previously used hack: (state.selection as any).$cursor.nodeBefore; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 758b4035e..4fa1fb13b 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -117,7 +117,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return this._activeAlignment; } @computed get textVcenter() { - return BoolCast(this.dataDoc?._layout_centered, BoolCast(Doc.UserDoc().layout_centered)); + return BoolCast(this.dataDoc?.text_centered, BoolCast(Doc.UserDoc().textCentered)); } @action @@ -367,7 +367,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { if (this.TextView && this.view && fontField !== 'fitBox') { - if (this.view.hasFocus()) { + const anchorNode = window.getSelection()?.anchorNode; + if (this.view.hasFocus() || (anchorNode && this.TextView.ProseRef?.contains(anchorNode))) { const attrs: { [key: string]: string } = {}; attrs[fontField] = value; const fmark = this.view.state.schema.marks['pF' + fontField.substring(1)].create(attrs); @@ -425,8 +426,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } vcenterToggle = () => { - if (this.dataDoc) this.dataDoc._layout_centered = !this.dataDoc._layout_centered; - else Doc.UserDoc()._layout_centered = !Doc.UserDoc()._layout_centered; + if (this.dataDoc) this.dataDoc.text_centered = !this.dataDoc.text_centered; + else Doc.UserDoc().textCentered = !Doc.UserDoc().textCentered; }; align = (view: EditorView | undefined, dispatch: undefined | ((tr: Transaction) => void), alignment: 'left' | 'right' | 'center') => { if (view && dispatch && this.RootSelected) { diff --git a/src/client/views/nodes/importBox/ImportElementBox.tsx b/src/client/views/nodes/importBox/ImportElementBox.tsx index 317719032..7e470a642 100644 --- a/src/client/views/nodes/importBox/ImportElementBox.tsx +++ b/src/client/views/nodes/importBox/ImportElementBox.tsx @@ -22,9 +22,7 @@ export class ImportElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <div style={{ backgroundColor: 'pink' }}> <DocumentView - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} // - LayoutTemplateString={undefined} Document={this.Document} isContentActive={returnFalse} addDocument={returnFalse} diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 31cd1603f..7e0375275 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -67,7 +67,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // Since this node is being rendered with a template, this method retrieves // the actual slide being rendered from the auto-generated rendering template @computed get slideDoc() { - return DocCast(this.Document.rootDocument, this.Document); + return this.rootDoc; } // this is the document in the workspaces that is targeted by the slide diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss index 18441f76e..bb43291ee 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.scss +++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss @@ -9,6 +9,8 @@ $headingHeight: 32px; .gptPopup-summary-box { position: fixed; + padding-left: 10px; + padding-right: 10px; top: 115px; left: 75px; width: 100%; @@ -48,7 +50,6 @@ $headingHeight: 32px; } label { - color: $textgrey; font-size: 12px; font-weight: 400; letter-spacing: 1px; @@ -73,7 +74,6 @@ $headingHeight: 32px; justify-content: center; align-items: center; height: $inputHeight; - background-color: white; width: 100%; pointer-events: all; diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index 67213382d..e3ee51424 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -411,7 +411,8 @@ export class GPTPopup extends ObservableReactComponent<object> { tooltip="Ask Firefly to create images" text="Ask Firefly" onClick={() => this.setMode(GPTPopupMode.FIREFLY)} - color={StrCast(Doc.UserDoc().userVariantColor)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} type={Type.TERT} style={{ width: '100%', @@ -426,7 +427,8 @@ export class GPTPopup extends ObservableReactComponent<object> { tooltip="Ask GPT to sort, tag, define, or filter your Docs!" text="Ask GPT" onClick={() => this.setMode(GPTPopupMode.USER_PROMPT)} - color={StrCast(Doc.UserDoc().userVariantColor)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} type={Type.TERT} style={{ width: '100%', @@ -445,7 +447,8 @@ export class GPTPopup extends ObservableReactComponent<object> { this.setMode(GPTPopupMode.QUIZ_RESPONSE); this.onQuizRandom?.(); }} - color={StrCast(Doc.UserDoc().userVariantColor)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} type={Type.TERT} style={{ width: '100%', @@ -517,6 +520,7 @@ export class GPTPopup extends ObservableReactComponent<object> { onChange={e => onChange(e.target.value)} onKeyDown={e => this.handleKeyPress(e, this._mode)} type="text" + style={{ color: SnappingManager.userColor }} placeholder={placeholder} /> <Button // @@ -524,7 +528,8 @@ export class GPTPopup extends ObservableReactComponent<object> { type={Type.TERT} icon={<AiOutlineSend />} iconPlacement="right" - color={SnappingManager.userVariantColor} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} onClick={() => this.callGpt(this._mode)} /> <DictationButton ref={r => (this._askDictation = r)} setInput={onChange} /> @@ -551,7 +556,7 @@ export class GPTPopup extends ObservableReactComponent<object> { </div> </div> <div key={rawSrc[0] + i + 'btn'} className="btn-container"> - <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} /> + <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} type={Type.TERT} /> </div> </> ))} @@ -561,7 +566,8 @@ export class GPTPopup extends ObservableReactComponent<object> { tooltip="Generate Again" onClick={() => this._imgTargetDoc && this.generateImage(this._imageDescription, this._imgTargetDoc, this._addToCollection)} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} - color={StrCast(Doc.UserDoc().userVariantColor)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} /> )} </div> @@ -603,14 +609,16 @@ export class GPTPopup extends ObservableReactComponent<object> { tooltip="Show originally selected text" // text="Selection" onClick={action(() => (this._showOriginal = true))} - color={StrCast(SettingsManager.userVariantColor)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} type={Type.TERT} /> <Button tooltip="Create a text Doc with this text and link to the text selection" // text="Transfer To Text" onClick={this.transferToText} - color={StrCast(SettingsManager.userVariantColor)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} type={Type.TERT} /> </> @@ -627,14 +635,16 @@ export class GPTPopup extends ObservableReactComponent<object> { } this.generateSummary(this._aiReferenceText); })} - color={StrCast(SettingsManager.userVariantColor)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} type={Type.TERT} /> <Button tooltip="Create Flashcards" // text="Create Flashcards" onClick={this.createFlashcards} - color={StrCast(SettingsManager.userVariantColor)} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} type={Type.TERT} /> </> @@ -644,7 +654,7 @@ export class GPTPopup extends ObservableReactComponent<object> { <div className="summarizing"> <span>{this._showOriginal ? 'Creating Cards...' : 'Summarizing'}</span> <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} /> - <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} /> + <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} type={Type.TERT} /> </div> )} </div> @@ -689,19 +699,19 @@ export class GPTPopup extends ObservableReactComponent<object> { placeholder="Ask GPT a question about the data..." id="search-input" className="searchBox-input" - style={{ width: '100%' }} + style={{ width: '100%', color: SnappingManager.userColor }} /> ) : ( <> - <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} /> - <Button tooltip="Chat with AI" text="Chat with AI" onClick={() => this.setChatEnabled(true)} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} /> + <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} type={Type.TERT} /> + <Button tooltip="Chat with AI" text="Chat with AI" onClick={() => this.setChatEnabled(true)} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} type={Type.TERT} /> </> ) ) : ( <div className="summarizing"> <span>Summarizing</span> <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} /> - <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} /> + <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} type={Type.TERT} /> </div> )} </div> @@ -718,7 +728,7 @@ export class GPTPopup extends ObservableReactComponent<object> { ); heading = (headingText: string) => ( - <div className="summary-heading"> + <div className="summary-heading" style={{ color: SnappingManager.userBackgroundColor }}> <label className="summary-text">{headingText}</label> {this._gptProcessing ? ( <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} /> @@ -734,7 +744,7 @@ export class GPTPopup extends ObservableReactComponent<object> { onClick={() => this._collectionContext && Doc.setDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag, 'remove')} /> {[GPTPopupMode.USER_PROMPT, GPTPopupMode.QUIZ_RESPONSE, GPTPopupMode.FIREFLY].includes(this._mode) && ( - <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this._mode = GPTPopupMode.GPT_MENU)} /> + <IconButton color={SettingsManager.userVariantColor} background={SettingsManager.userColor} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this._mode = GPTPopupMode.GPT_MENU)} /> )} </> )} @@ -743,7 +753,7 @@ export class GPTPopup extends ObservableReactComponent<object> { render() { return ( - <div className="gptPopup-summary-box" style={{ display: SnappingManager.ChatVisible ? 'flex' : 'none', overflow: 'auto' }}> + <div className="gptPopup-summary-box" style={{ background: SnappingManager.userColor, color: SnappingManager.userBackgroundColor, display: SnappingManager.ChatVisible ? 'flex' : 'none', overflow: 'auto' }}> {(() => { //prettier-ignore switch (this._mode) { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 73c2f5eb8..ed2f661e6 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -30,6 +30,7 @@ import { AnchorMenu } from './AnchorMenu'; import { Annotation } from './Annotation'; import { GPTPopup } from './GPTPopup/GPTPopup'; import './PDFViewer.scss'; +import { DocumentViewProps } from '../nodes/DocumentContentsView'; if (window?.Worker) GlobalWorkerOptions.workerSrc = 'files/pdf.worker.min.mjs'; export * from 'pdfjs-dist/build/pdf.mjs'; @@ -521,7 +522,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter]; opaqueFilter = () => [...this._props.childFilters(), ClientUtils.noDragDocsFilter, ...(SnappingManager.CanEmbed && this._props.isContentActive() ? [] : [ClientUtils.OpaqueBackgroundFilter])]; - childStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => { + childStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps & DocumentViewProps>, property: string) => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (this.inlineTextAnnotations.includes(doc) || this._props.isContentActive() === false) return 'none'; const isInk = doc.layout_isSvg && !props?.LayoutTemplateString; diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index 841546a04..86e28a768 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -4,7 +4,7 @@ import { Doc, DocListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { List } from '../../../fields/List'; import { ComputedField } from '../../../fields/ScriptField'; -import { DocCast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { DocCast, ImageCast, ImageCastToNameType, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -43,7 +43,7 @@ export class FaceRecognitionHandler { * Loads an image */ private static loadImage = (imgUrl: ImageField): Promise<HTMLImageElement> => { - const [name, type] = imgUrl.url.href.split('.'); + const [name, type] = ImageCastToNameType(imgUrl.url.href); const imageURL = `${name}_o.${type}`; return new Promise((resolve, reject) => { diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx index f1d5f2cfe..a91ec23b8 100644 --- a/src/client/views/smartdraw/DrawingFillHandler.tsx +++ b/src/client/views/smartdraw/DrawingFillHandler.tsx @@ -1,7 +1,6 @@ -import { imageUrlToBase64 } from '../../../ClientUtils'; import { Doc, StrListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; -import { DocCast, ImageCast } from '../../../fields/Types'; +import { DocCast, ImageCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { Upload } from '../../../server/SharedMediaTypes'; import { gptDescribeImage } from '../../apis/gpt/GPT'; @@ -16,17 +15,20 @@ export class DrawingFillHandler { static drawingToImage = async (drawing: Doc, strength: number, user_prompt: string, styleDoc?: Doc) => { const tags = StrListCast(drawing.$tags).map(tag => tag.slice(1)); const styles = tags.filter(tag => FireflyStylePresets.has(tag)); - const styleDocs = !Doc.Links(drawing).length - ? styleDoc && !tags.length - ? [styleDoc] - : [] - : Doc.Links(drawing) - .map(link => Doc.getOppositeAnchor(link, drawing)) - .map(anchor => anchor && DocCast(anchor.embedContainer)); - const styleUrl = await DocumentView.GetDocImage(styleDocs.filter(doc => doc?.data instanceof ImageField).lastElement())?.then(styleImg => { - const hrefParts = ImageCast(styleImg).url.href.split('.'); - return `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`; - }); + const styleDocs = [drawing].concat( + drawing, + ...Doc.Links(drawing) + .map(link => Doc.getOppositeAnchor(link, drawing)) + .map(anchor => DocCast(anchor?.annotationOn, anchor)) + .map(anchor => anchor!), + ...(styleDoc ? [styleDoc] : []) + ); + const styleUrl = tags.length + ? undefined + : await DocumentView.GetDocImage(styleDocs.filter(doc => doc?.data instanceof ImageField).lastElement())?.then(styleImg => { + const hrefParts = ImageCast(styleImg).url.href.split('.'); + return `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`; + }); return DocumentView.GetDocImage(drawing)?.then(imageField => { if (imageField) { const aspectRatio = (drawing.width as number) / (drawing.height as number); @@ -39,41 +41,42 @@ export class DrawingFillHandler { const { href } = ImageCast(imageField).url; const hrefParts = href.split('.'); const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`; - return imageUrlToBase64(structureUrl) - .then(gptDescribeImage) - .then((prompt, newPrompt = user_prompt || prompt) => - Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structureUrl, strength, presets: styles, styleUrl }) - .then(res => { - const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { _width: 400, _height: 400 }); - drawing.$ai_firefly_generatedDocs = genratedDocs; - (res as Upload.ImageInformation[]).map(info => - Doc.AddDocToList( - genratedDocs, - undefined, - Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { - ai: 'firefly', - tags: new List<string>(['@ai']), - title: newPrompt, - _data_usePath: 'alternate:hover', - data_alternates: new List<Doc>([drawing]), - ai_firefly_prompt: newPrompt, - _width: 500, - data_nativeWidth: info.nativeWidth, - data_nativeHeight: info.nativeHeight, - }), - undefined, - undefined, - true - ) - ); - if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight); - }) - .catch(e => { - if (e.toString().includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + e.toString().replace(/^[^"]*/, ''))) { - window.open(`https://www.dropbox.com/oauth2/authorize?client_id=${DashDropboxId}&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox`, '_blank')?.focus(); - } else alert(e.toString()); - }) - ); // prettier-ignore:q + return gptDescribeImage(user_prompt, structureUrl).then(newPrompt => + Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structureUrl, strength, presets: styles, styleUrl }) + .then(res => { + const error = ('error' in res && (res.error as string)) || ''; + if (error.includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + error.replace(/^[^"]*/, ''))) { + window.open(`https://www.dropbox.com/oauth2/authorize?client_id=${DashDropboxId}&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox`, '_blank')?.focus(); + return; + } + const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { title: StrCast(drawing.title) + ' AI Images', _width: 400, _height: 400 }); + drawing.$ai_firefly_generatedDocs = genratedDocs; + (res as Upload.ImageInformation[]).map(info => + Doc.AddDocToList( + genratedDocs, + undefined, + Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { + ai: 'firefly', + tags: new List<string>(['@ai']), + title: newPrompt, + _data_usePath: 'alternate:hover', + data_alternates: new List<Doc>([drawing]), + ai_firefly_prompt: newPrompt, + _width: 500, + data_nativeWidth: info.nativeWidth, + data_nativeHeight: info.nativeHeight, + }), + undefined, + undefined, + true + ) + ); + if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight); + }) + .catch(e => { + alert(e.toString()); + }) + ); // prettier-ignore:q } }); }; diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 00114a3f9..18e30b3c2 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -59,7 +59,7 @@ export class TopBar extends ObservableReactComponent<object> { return SettingsManager.userVariantColor; } @computed get backgroundColor() { - return PingManager.Instance.IsBeating ? SettingsManager.userBackgroundColor : Colors.MEDIUM_GRAY; + return SettingsManager.userBackgroundColor; } @observable happyHeart: boolean = PingManager.Instance.IsBeating; @@ -86,6 +86,7 @@ export class TopBar extends ObservableReactComponent<object> { onClick={this.navigateToHome} icon={<FontAwesomeIcon icon={DocListCast(Doc.MySharedDocs.data_dashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'portrait' : 'home'} />} color={this.color} + background={this.backgroundColor} /> ) : ( <div className="logo-container"> @@ -166,6 +167,7 @@ export class TopBar extends ObservableReactComponent<object> { tooltip="Open Dashboards" size={Size.SMALL} color={this.color} + background={this.backgroundColor} style={{ fontWeight: 700, fontSize: '1rem' }} onClick={(e: React.MouseEvent) => { const dashView = Doc.ActiveDashboard && DocumentView.getDocumentView(Doc.ActiveDashboard); @@ -191,22 +193,27 @@ export class TopBar extends ObservableReactComponent<object> { type={Type.TERT} color={SettingsManager.userColor} background={this.variantColor} - onClick={() => { - SharingManager.Instance.open(undefined, Doc.ActiveDashboard); - }} + onClick={() => SharingManager.Instance.open(undefined, Doc.ActiveDashboard)} /> ) : null} - <IconButton tooltip="Issue Reporter ⌘I" size={Size.SMALL} color={this.color} onClick={ReportManager.Instance.open} icon={<FaBug />} /> + <IconButton tooltip="Issue Reporter ⌘I" size={Size.SMALL} color={this.color} background={this.backgroundColor} onClick={ReportManager.Instance.open} icon={<FaBug />} /> <Flip key={this._flipDocumentation}> - <IconButton tooltip="Documentation ⌘D" size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} /> + <IconButton + tooltip="Documentation ⌘D" + size={Size.SMALL} + color={this.color} + background={this.backgroundColor} + onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} + icon={<FontAwesomeIcon icon="question-circle" />} + /> </Flip> - <IconButton tooltip="Settings ⌘⇧S" size={Size.SMALL} color={this.color} onClick={SettingsManager.Instance.openMgr} icon={<FontAwesomeIcon icon="cog" />} /> + <IconButton tooltip="Settings ⌘⇧S" size={Size.SMALL} color={this.color} background={this.backgroundColor} onClick={SettingsManager.Instance.openMgr} icon={<FontAwesomeIcon icon="cog" />} /> <IconButton size={Size.SMALL} onClick={ServerStats.Instance.open} - type={Type.TERT} tooltip={'Server is ' + (PingManager.Instance.IsBeating ? '' : 'NOT ') + 'running ' + (upToDate ? DashVersion : 'out of date version:' + DashVersion)} color={this.happyHeart ? (upToDate ? Colors.LIGHT_BLUE : Colors.YELLOW) : Colors.ERROR_RED} + background={PingManager.Instance.IsBeating ? SettingsManager.userBackgroundColor : Colors.MEDIUM_GRAY} icon={<FontAwesomeIcon icon={this.happyHeart ? 'heart' : 'heart-broken'} />} /> {/* <Button text={'Logout'} borderRadius={5} hoverStyle={'gray'} backgroundColor={Colors.DARK_GRAY} color={this.color} fontSize={FontSize.SECONDARY} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> */} diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 4bf04da5c..e48337651 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -43,10 +43,10 @@ export namespace Field { * @param doc doc containing key * @param key field key to display * @param showComputedValue whether copmuted function should display its value instead of its function + * @param schemaCell * @returns string representation of the field */ export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean, schemaCell?: boolean): string { - const isOnDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, '')); const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const valFunc = (field: FieldType): string => { const res = @@ -64,7 +64,9 @@ export namespace Field { .trim() .replace(/^new List\((.*)\)$/, '$1'); }; - return !Field.IsField(cfield) ? (key.startsWith('_') ? '=' : '') : (isOnDelegate ? '=' : '') + valFunc(cfield); + const notOnTemplate = !key.startsWith('_') || doc[DocLayout] === doc; + const isOnDelegate = notOnTemplate && !Doc.IsDataProto(doc) && ((key.startsWith('_') && !Field.IsField(cfield)) || Object.keys(doc).includes(key.replace(/^_/, ''))); + return (isOnDelegate ? '=' : '') + (!Field.IsField(cfield) ? '' : valFunc(cfield)); } export function toScriptString(field: FieldType, schemaCell?: boolean) { switch (typeof field) { @@ -405,9 +407,9 @@ export class Doc extends RefField { } @computed get __DATA__(): Doc { const self = this[SelfProxy]; - return self.resolvedDataDoc && !self.isTemplateForField ? self : Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self); + return self.rootDocument && !self.isTemplateForField ? self : Doc.GetProto(Cast(self[DocLayout].rootDocument, Doc, null) || self); } - @computed get __LAYOUT__(): Doc | undefined { + @computed get __LAYOUT__(): Doc { const self = this[SelfProxy]; const templateLayoutDoc = Cast(Doc.LayoutField(self), Doc, null); if (templateLayoutDoc) { @@ -420,7 +422,7 @@ export class Doc extends RefField { } return Cast(self['layout_' + templateLayoutDoc.title + '(' + renderFieldKey + ')'], Doc, null) || templateLayoutDoc; } - return undefined; + return self; } public async [HandleUpdate](diff: { $set: { [key: string]: FieldType } } | { $unset?: unknown }) { @@ -685,15 +687,11 @@ export namespace Doc { } export function MakeEmbedding(doc: Doc, id?: string) { - const embedding = (!GetT(doc, 'isDataDoc', 'boolean', true) && doc.proto) || doc.type === DocumentType.CONFIG ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); - const layout = Doc.LayoutField(embedding); - if (layout instanceof Doc && layout !== embedding && layout === Doc.Layout(embedding)) { - Doc.SetLayout(embedding, Doc.MakeEmbedding(layout)); - } + const embedding = (!Doc.IsDataProto(doc) && doc.proto) || doc.type === DocumentType.CONFIG ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); embedding.createdFrom = doc; - embedding.proto_embeddingId = doc.$proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; - !Doc.GetT(embedding, 'title', 'string', true) && (embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`)); embedding.author = ClientUtils.CurrentUserEmail(); + embedding.proto_embeddingId = doc.$proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; + embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`); return embedding; } @@ -766,9 +764,8 @@ export namespace Doc { // prune doc and do nothing } else if ( !Doc.IsSystem(docAtKey) && - (key.includes('layout[') || - key.startsWith('layout') || // - ['embedContainer', 'annotationOn', 'proto'].includes(key) || + (key.startsWith('layout') || + ['embedContainer', 'annotationOn', 'proto'].includes(key) || // (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === ClientUtils.CurrentUserEmail())) ) { assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); @@ -871,15 +868,15 @@ export namespace Doc { const expandedLayoutFieldKey = 'layout_' + templateLayoutDoc.title + '(' + templateField + ')'; let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey]; - if (templateLayoutDoc.resolvedDataDoc instanceof Promise) { + if (templateLayoutDoc.rootDocument instanceof Promise) { expandedTemplateLayout = undefined; _pendingMap.add(targetDoc[Id] + expandedLayoutFieldKey); } else if (expandedTemplateLayout === undefined && !_pendingMap.has(targetDoc[Id] + expandedLayoutFieldKey)) { - if (templateLayoutDoc.resolvedDataDoc === targetDoc[DocData]) { + if (DocCast(templateLayoutDoc.rootDocument)?.[DocData] === targetDoc[DocData]) { expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params } else { // eslint-disable-next-line no-param-reassign - templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype + templateLayoutDoc.rootDocument && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype if (!targetDoc[expandedLayoutFieldKey]) { _pendingMap.add(targetDoc[Id] + expandedLayoutFieldKey); setTimeout( @@ -888,7 +885,6 @@ export namespace Doc { const dataDoc = Doc.GetProto(targetDoc); newLayoutDoc.rootDocument = targetDoc; newLayoutDoc.embedContainer = targetDoc; - newLayoutDoc.resolvedDataDoc = dataDoc; newLayoutDoc.acl_Guest = SharingPermissions.Edit; if (dataDoc[templateField] === undefined && (templateLayoutDoc[templateField] as List<Doc>)?.length) { dataDoc[templateField] = ObjectField.MakeCopy(templateLayoutDoc[templateField] as List<Doc>); @@ -912,9 +908,9 @@ export namespace Doc { console.log('Warning: GetLayoutDataDocPair childDoc not defined'); return { layout: childDoc, data: childDoc }; } - const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc; + const data = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc; const templateRoot = DocCast(containerDoc?.rootDocument); - return { layout: Doc.expandTemplateLayout(childDoc, templateRoot), data: resolvedDataDoc }; + return { layout: Doc.expandTemplateLayout(childDoc, templateRoot), data }; } export function FindReferences(infield: Doc | List<Doc>, references: Set<Doc>, system: boolean | undefined) { @@ -1083,7 +1079,7 @@ export namespace Doc { export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) { if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) { - if (target.resolvedDataDoc) { + if (target.rootDocument) { target[targetKey] = new PrefetchProxy(templateDoc); } else { titleTarget && (Doc.GetProto(target).title = titleTarget); @@ -1116,10 +1112,10 @@ export namespace Doc { Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue); } // get the layout string that the template uses to specify its layout - const templateFieldLayoutString = StrCast(Doc.LayoutField(Doc.Layout(templateField))); + const templateFieldLayoutString = StrCast(Doc.LayoutField(templateField[DocLayout])); // change it to render the target metadata field instead of what it was rendering before and assign it to the template field layout document. - Doc.Layout(templateField).layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`); + templateField[DocLayout].layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`); return true; } @@ -1154,18 +1150,15 @@ export namespace Doc { // the document containing the view layout information - will be the Document itself unless the Document has // a layout field or 'layout' is given. - export function Layout(doc: Doc, layout?: Doc): Doc { - const overrideLayout = layout && Cast(doc[layout.title + '(' + StrCast(layout.isTemplateForField, 'data') + ')'], Doc, null); - return overrideLayout || doc[DocLayout] || doc; - } - export function SetLayout(doc: Doc, layout: Doc | string) { - doc[StrCast(doc.layout_fieldKey, 'layout')] = layout; + export function Layout(doc: Doc, template?: Doc): Doc { + const expandedTemplate = template && Cast(doc['layout_' + template.title + '(' + StrCast(template.isTemplateForField, 'data') + ')'], Doc, null); + return expandedTemplate || doc[DocLayout]; } export function LayoutField(doc: Doc) { return doc[StrCast(doc.layout_fieldKey, 'layout')]; } export function LayoutFieldKey(doc: Doc, templateLayoutString?: string): string { - const match = StrCast(templateLayoutString || Doc.Layout(doc).layout).match(/fieldKey={'([^']+)'}/); + const match = StrCast(templateLayoutString || doc[DocLayout].layout).match(/fieldKey={'([^']+)'}/); return match?.[1] || ''; // bcz: TODO check on this . used to always reference 'layout', now it uses the layout speicfied by the current layout_fieldKey } export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) { @@ -1301,7 +1294,7 @@ export namespace Doc { highlightedDocs.add(doc); doc[Highlight] = true; doc[Animation] = presentationEffect; - if (dataAndDisplayDocs && !doc.resolvedDataDoc) { + if (dataAndDisplayDocs && !doc.rootDocument) { // if doc is a layout template then we don't want to highlight the proto since that will be the entire template, not just the specific layout field highlightedDocs.add(doc[DocData]); doc[DocData][Highlight] = true; @@ -1324,17 +1317,7 @@ export namespace Doc { } export function getDocTemplate(doc?: Doc) { - return !doc - ? undefined - : doc.isTemplateDoc - ? doc - : Cast(doc.dragFactory, Doc, null)?.isTemplateDoc - ? doc.dragFactory - : Cast(Doc.Layout(doc), Doc, null)?.isTemplateDoc - ? Cast(Doc.Layout(doc), Doc, null).resolvedDataDoc - ? Doc.Layout(doc).proto - : Doc.Layout(doc) - : undefined; + return !doc ? undefined : doc.isTemplateDoc ? doc : Cast(doc.dragFactory, Doc, null)?.isTemplateDoc ? doc.dragFactory : doc[DocLayout].isTemplateDoc ? (doc[DocLayout].rootDocument ? doc[DocLayout].proto : doc[DocLayout]) : undefined; } export function toggleLockedPosition(doc: Doc) { @@ -1478,9 +1461,9 @@ export namespace Doc { const docText = (async (tdoc:Doc) => { switch (tdoc.type) { case DocumentType.PDF: return curDescription || StrCast(tdoc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text - case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(Doc.LayoutField(tdoc), '_o') ?? '') + case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(tdoc[Doc.LayoutFieldKey(tdoc)], '_o') ?? '') .then(hrefBase64 => gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.')); - case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutFieldKey(tdoc)]).Text; + case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutFieldKey(tdoc)])?.Text ?? StrCast(tdoc[Doc.LayoutFieldKey(tdoc)]); default: return StrCast(tdoc.title).startsWith("Untitled") ? "" : StrCast(tdoc.title); }}); // prettier-ignore return docText(doc).then(text => (doc['$' + Doc.LayoutFieldKey(doc) + '_description'] = text)); @@ -1736,7 +1719,7 @@ ScriptingGlobals.add(function idToDoc(id: string): Doc { }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function renameEmbedding(doc: Doc) { - return StrCast(doc.$title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; + return StrCast(doc.title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getProto(doc: Doc) { diff --git a/src/fields/Types.ts b/src/fields/Types.ts index af9cb1180..dc7516f38 100644 --- a/src/fields/Types.ts +++ b/src/fields/Types.ts @@ -131,9 +131,13 @@ export function PDFCast(field: FieldResult, defaultVal: PdfField | null = null) export function ImageCast(field: FieldResult, defaultVal: ImageField | null = null) { return Cast(field, ImageField, defaultVal); } -export function ImageCastWithSuffix(field: FieldResult, suffix: string, defaultVal: ImageField | null = null) { +export function ImageCastToNameType(field: FieldResult, defaultVal: ImageField | null = null) { const href = ImageCast(field, defaultVal)?.url.href; - return href ? `${href.split('.')[0]}${suffix}.${href.split('.')[1]}` : null; + return href ? [href.replace(/.[^.]*$/, ''), href.split('.').lastElement()] : ["", ""]; +} +export function ImageCastWithSuffix(field: FieldResult, suffix: string, defaultVal: ImageField | null = null) { + const [name, type] = ImageCastToNameType(field, defaultVal); + return name ? `${name}${suffix}.${type}` : null; } export function FieldValue<T extends FieldType, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>; diff --git a/src/fields/util.ts b/src/fields/util.ts index abbe543e8..205c500a5 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -303,30 +303,23 @@ export function SetPropSetterCb(prop: string, propSetter: ((target: Doc, value: // // target should be either a Doc or ListImpl. receiver should be a Proxy<Doc> Or List. // -export function setter(target: ListImpl<FieldType> | Doc, inProp: string | symbol | number, value: unknown, receiver: Doc | ListImpl<FieldType>): boolean { - if (!inProp) { +export function setter(target: ListImpl<FieldType> | Doc, prop: string | symbol | number, value: unknown, receiver: Doc | ListImpl<FieldType>): boolean { + if (!prop) { console.log('WARNING: trying to set an empty property. This should be fixed. '); return false; } - let prop = inProp; - const effectiveAcl = inProp === 'constructor' || typeof inProp === 'symbol' ? AclAdmin : GetPropAcl(target, prop); + const effectiveAcl = prop === 'constructor' || typeof prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't if (typeof prop === 'string' && prop.startsWith('acl_') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value as SharingPermissions))) return true; - if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('$')) { - prop = prop.substring(1); - if (target.__DATA__ instanceof Doc) { - target.__DATA__[prop] = value as FieldResult; - return true; - } + if (target instanceof Doc && typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('$')) { + target.__DATA__[prop.substring(1)] = value as FieldResult; + return true; } - if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_')) { - if (!prop.startsWith('__')) prop = prop.substring(1); - if (target.__LAYOUT__ instanceof Doc) { - target.__LAYOUT__[prop] = value as FieldResult; - return true; - } + if (target instanceof Doc && typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_') && !prop.startsWith('__')) { + target.__LAYOUT__[prop.substring(1)] = value as FieldResult; + return true; } if (target.__fieldTuples[prop] instanceof ComputedField) { if (target.__fieldTuples[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts index e75ede9df..07428798c 100644 --- a/src/server/ApiManagers/FireflyManager.ts +++ b/src/server/ApiManagers/FireflyManager.ts @@ -117,7 +117,6 @@ export default class FireflyManager extends ApiManager { generateImage = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, seed?: number) => { let body = `{ "prompt": "${prompt}", "size": { "width": ${width}, "height": ${height}} }`; if (seed) { - console.log('RECEIVED SEED', seed); body = `{ "prompt": "${prompt}", "size": { "width": ${width}, "height": ${height}}, "seeds": [${seed}]}`; } const fetched = this.getBearerToken().then(response => diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index c9d5df547..7c55e4a42 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -126,11 +126,8 @@ export default class UploadManager extends ApiManager { secureHandler: async ({ req, res }) => { const { sources } = req.body; if (Array.isArray(sources)) { - const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source))); - res.send(results); - return; - } - res.send(); + res.send(await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)))); + } else res.send(); }, }); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index a2747257a..ed109d8f7 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -450,14 +450,12 @@ export namespace DashUploadUtils { * 3) the size of the image, in bytes (4432130) * 4) the content type of the image, i.e. image/(jpeg | png | ...) */ - export const UploadImage = async (source: string, filename?: string, prefix: string = ''): Promise<Upload.ImageInformation | Error> => { - const result = await InspectImage(source); - if (result instanceof Error) { - return { name: result.name, message: result.message }; - } - const outputFile = filename || result.filename || ''; - return UploadInspectedImage(result, outputFile, prefix, isLocal().exec(source) || source.startsWith('data:') ? true : false); - }; + export const UploadImage = (source: string, filename?: string, prefix: string = ''): Promise<Upload.ImageInformation | Error> => + InspectImage(source).then(async result => + result instanceof Error + ? ({ name: result.name, message: result.message } as Error) // + : UploadInspectedImage(result, filename || result.filename || '', prefix, isLocal().exec(source) || source.startsWith('data:') ? true : false) + ); type md5 = 'md5'; type falsetype = false; |