/* eslint-disable jsx-a11y/alt-text */ /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from 'browndash-components'; import { action, untracked } from 'mobx'; import { extname } from 'path'; import * as React from 'react'; 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 { DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types'; import { AudioAnnoState } from '../../server/SharedMediaTypes'; import { emptyPath } from '../../Utils'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { IsFollowLinkScript } from '../documents/DocUtils'; import { SnappingManager } from '../util/SnappingManager'; import { undoBatch, UndoManager } from '../util/UndoManager'; import { TreeSort } from './collections/TreeSort'; import { Colors } from './global/globalEnums'; import { KeywordBox } from './KeywordBox'; import { DocumentView, DocumentViewProps } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; import { StyleProp } from './StyleProp'; import './StyleProvider.scss'; import { IconTagBox } from './nodes/IconTagBox'; function toggleLockedPosition(doc: Doc) { UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground'); } function togglePaintView(e: React.MouseEvent, doc: Opt, props: Opt) { const scriptProps = { this: doc, _readOnly_: false, documentView: props?.DocumentView?.(), value: undefined, }; e.stopPropagation(); ScriptCast(doc?.onPaint)?.script.run(scriptProps); } export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: number) { const style: { [key: string]: any } = {}; const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position']; const replacer = (match: any, expr: string) => // bcz: this executes a script to convert a property expression string: { script } into a value ScriptField.MakeFunction(expr, { this: Doc.name, scale: 'number' })?.script.run({ this: doc, scale }).result?.toString() ?? ''; divKeys.forEach((prop: string) => { const p = (props as any)[prop]; typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); }); return style; } export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) { return `M ${pw * 0.5} ${ph * inset} C ${pw * 0.6} ${ph * inset} ${pw * (1 - 2 * inset)} 0 ${pw * (1 - inset)} ${ph * inset} C ${pw} ${ph * (2 * inset)} ${pw * (1 - inset)} ${ph * 0.25} ${pw * (1 - inset)} ${ph * 0.3} C ${ pw * (1 - inset) } ${ph * 0.4} ${pw} ${ph * (1 - 2 * inset)} ${pw * (1 - inset)} ${ph * (1 - inset)} C ${pw * (1 - 2 * inset)} ${ph} ${pw * 0.6} ${ph * (1 - inset)} ${pw * 0.5} ${ph * (1 - inset)} C ${pw * 0.3} ${ph * (1 - inset)} ${pw * (2 * inset)} ${ph} ${ pw * inset } ${ph * (1 - inset)} C 0 ${ph * (1 - 2 * inset)} ${pw * inset} ${ph * 0.8} ${pw * inset} ${ph * 0.75} C ${pw * inset} ${ph * 0.7} 0 ${ph * (2 * inset)} ${pw * inset} ${ph * inset} C ${pw * (2 * inset)} 0 ${pw * 0.25} ${ph * inset} ${ pw * 0.5 } ${ph * inset}`; } let _filterOpener: () => void; export function SetFilterOpener(func: () => void) { _filterOpener = func; } // a preliminary implementation of a dash style sheet for setting rendering properties of documents nested within a Tab // export function DefaultStyleProvider(doc: Opt, props: Opt, property: string): any { const remoteDocHeader = 'author;author_date;noMargin'; const isCaption = property.includes(':caption'); const isAnchor = property.includes(':anchor'); 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 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 { DocumentView: docView, fieldKey: fieldKeyProp, styleProvider, pointerEvents, isGroupActive, isDocumentActive, containerViewPath, childFilters, hideCaptions, showTitle, childFiltersByRanges, renderDepth, docViewPath, LayoutTemplateString, disableBrushing, NativeDimScaling, PanelWidth, PanelHeight, isSelected, isHovering, } = props || {}; // extract props that are not shared between fieldView and documentView props. const componentView = docView?.()?.ComponentView; const fieldKey = fieldKeyProp ? fieldKeyProp + '_' : isCaption ? 'caption_' : ''; const isInk = () => layoutDoc?._layout_isSvg && !LayoutTemplateString; const lockedPosition = () => doc && BoolCast(doc._lockedPosition); const titleHeight = () => styleProvider?.(doc, props, StyleProp.TitleHeight); const backgroundCol = () => styleProvider?.(doc, props, StyleProp.BackgroundColor + ':nonTransparent' + (isNonTransparentLevel + 1)); const color = () => styleProvider?.(doc, props, StyleProp.Color); const opacity = () => styleProvider?.(doc, props, StyleProp.Opacity); const layoutShowTitle = () => styleProvider?.(doc, props, StyleProp.ShowTitle); // prettier-ignore switch (property.split(':')[0]) { case StyleProp.TreeViewIcon: { const img = ImageCast(doc?.icon ?? doc?.[doc ? Doc.LayoutFieldKey(doc) : ""]); if (img) { const ext = extname(img.url.href); const url = doc?.icon ? img.url.href : img.url.href.replace(ext, '_s' + ext); return ; } return Doc.toIcon(doc, isOpen); } case StyleProp.TreeViewSortings: { const allSorts: { [key: string]: { color: string; icon: JSX.Element | string } | undefined } = {}; allSorts[TreeSort.AlphaDown] = { color: Colors.MEDIUM_BLUE, icon: }; allSorts[TreeSort.AlphaUp] = { color: 'crimson', icon: }; if (doc?._type_collection === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', icon: 'Z' }; allSorts[TreeSort.WhenAdded] = { color: 'darkgray', icon: }; return allSorts; } case StyleProp.Highlighting: if (doc && (Doc.IsSystem(doc) || doc.type === DocumentType.FONTICON)) return undefined; if (doc && !doc.layout_disableBrushing && !disableBrushing) { const selected = DocumentView.getViews(doc).filter(dv => dv.IsSelected).length; const highlightIndex = Doc.GetBrushHighlightStatus(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0); const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? "black" : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex]; if (highlightIndex) { return { highlightStyle: doc.isGroup ? "dotted": highlightStyle, highlightColor, highlightIndex, highlightStroke: layoutDoc?.layout_isSvg, }; } } return undefined; case StyleProp.DocContents: return undefined; case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey'; case StyleProp.Opacity: return componentView?.isUnstyledView?.() ? 1 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null)); case StyleProp.FontColor: return StrCast(doc?.[fieldKey + 'fontColor'], isCaption ? lightOrDark(backgroundCol()) : StrCast(Doc.UserDoc().fontColor, color())); case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(Doc.UserDoc().fontSize)); case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily)); case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(Doc.UserDoc().fontWeight)); case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent'))); case StyleProp.ShowCaption: return hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption); case StyleProp.TitleHeight: return Math.min(4,(docView?.().screenToViewTransform().Scale ?? 1)) * NumCast(Doc.UserDoc().headerHeight,30); case StyleProp.ShowTitle: return ( (doc && !componentView?.isUnstyledView?.() && !LayoutTemplateString && !doc.presentation_targetDoc && showTitle?.() !== '' && StrCast( doc._layout_showTitle, showTitle?.() || (!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.FUNCPLOT, DocumentType.LABEL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any) ? doc.author === ClientUtils.CurrentUserEmail() ? StrCast(Doc.UserDoc().layout_showTitle) : remoteDocHeader : '') )) || '' ); case StyleProp.Color: { if (SnappingManager.LastPressedBtn === doc?.[Id]) return SnappingManager.userBackgroundColor; if (Doc.IsSystem(doc!)) return SnappingManager.userColor; if (doc?.type === DocumentType.FONTICON) return SnappingManager.userColor; const docColor: Opt = StrCast(doc?.[fieldKey + 'color'], StrCast(doc?._color)); if (docColor) return docColor; const backColor = backgroundCol(); return backColor ? lightOrDark(backColor) : undefined; } case StyleProp.BorderRounding: { const rounding = StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.layout_borderRounding, doc?._type_collection === CollectionViewType.Pile ? '50%' : '')); return (doc?.[StrCast(doc?.layout_fieldKey)] instanceof Doc || doc?.isTemplateDoc) ? StrCast(doc._layout_borderRounding,rounding) : rounding; } case StyleProp.BorderPath: { const borderPath = Doc.IsComicStyle(doc) && renderDepth && !doc?.layout_isSvg && { path: wavyBorderPath(PanelWidth?.() || 0, PanelHeight?.() || 0), fill: wavyBorderPath(PanelWidth?.() || 0, PanelHeight?.() || 0, 0.08), width: 3 }; return !borderPath ? null : { clipPath: `path('${borderPath.path}')`, jsx: (
), }; } case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._type_collection as any) || (doc?.type === DocumentType.RTF && !layoutShowTitle()?.includes('noMargin')) || doc?.type === DocumentType.LABEL) && layoutShowTitle() && !StrCast(doc?.layout_showTitle).includes(':hover') ? titleHeight() : 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 = StrCast(doc?.[fieldKey+alternate], StrCast(doc?.['backgroundColor' +alternate], isCaption ? 'rgba(0,0,0,0.4)' : '')); if (doc?.[StrCast(doc?.layout_fieldKey)] instanceof Doc) docColor = StrCast(doc._backgroundColor,docColor) // 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.RTF: docColor = docColor || Colors.LIGHT_GRAY; break; case DocumentType.LINK: docColor = (isAnchor ? docColor : undefined); break; case DocumentType.INK: docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; break; case DocumentType.EQUATION: docColor = docColor || 'transparent'; break; case DocumentType.LABEL: docColor = docColor || Colors.LIGHT_GRAY; break; case DocumentType.BUTTON: docColor = docColor || Colors.LIGHT_GRAY; break; case DocumentType.IMG: case DocumentType.WEB: case DocumentType.PDF: case DocumentType.MAP: case DocumentType.SCREENSHOT: case DocumentType.VID: docColor = docColor || (Colors.LIGHT_GRAY); break; case DocumentType.COL: docColor = docColor || (doc && Doc.IsSystem(doc) ? SnappingManager.userBackgroundColor : doc?.annotationOn ? '#00000010' // faint interior for collections on PDFs, images, etc : doc?.isGroup ? undefined : doc?._type_collection === CollectionViewType.Stacking ? (Colors.DARK_GRAY) : Cast((renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (Colors.MEDIUM_GRAY)); break; // if (doc._type_collection !== CollectionViewType.Freeform && doc._type_collection !== CollectionViewType.Time) return "rgb(62,62,62)"; default: docColor = docColor || (Colors.WHITE); } if (isNonTransparent && isNonTransparentLevel < 9 && (!docColor || docColor === 'transparent') && doc?.embedContainer && styleProvider) { return styleProvider(DocCast(doc.embedContainer), props, StyleProp.BackgroundColor+":nonTransparent"+(isNonTransparentLevel+1)); } return (docColor && !doc) ? DashColor(docColor).fade(0.5).toString() : docColor; } case StyleProp.BoxShadow: { if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow) if (doc.layout_boxShadow === 'standard') return Shadows.STANDARD_SHADOW; if (IsFollowLinkScript(doc?.onClick) && Doc.Links(doc).length && !layoutDoc?.layout_isSvg) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em'); switch (doc?.type) { case DocumentType.COL: return StrCast( doc?.layout_boxShadow, doc?._type_collection === CollectionViewType.Pile ? '4px 4px 10px 2px' : lockedPosition() || doc?.isGroup || LayoutTemplateString ? undefined // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide) : `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}` ); case DocumentType.LABEL: if (doc?.annotationOn !== undefined) return 'black 2px 2px 1px'; // eslint-disable-next-line no-fallthrough default: return doc.z ? `#9c9396 ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow : containerViewPath?.().lastElement()?.Document._freeform_useClusters ? `${backgroundCol()} ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent : NumCast(doc.group, -1) !== -1 ? `gray ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent : lockedPosition() ? undefined // if it's a background & has a cluster color, make the shadow spread really big : fieldKey.includes('_inline') // if doc is an inline document in a text box ? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0vw 0vw 0.1vw')}` : DocCast(doc.embedContainer)?.type === DocumentType.RTF && !isInk() // if doc is embedded in a text document (but not an inline) ? `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}` : StrCast(doc.layout_boxShadow, ''); } } case StyleProp.PointerEvents: if (componentView?.dontRegisterView?.()) return 'all'; if (StrCast(doc?.pointerEvents)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc if (SnappingManager.ExploreMode || doc?.layout_unrendered) return isInk() ? 'visiblePainted' : 'all'; if (pointerEvents?.() === 'none') return 'none'; if (opacity() === 0) return 'none'; if (isGroupActive?.() ) return isInk() ? 'visiblePainted': (doc?. isGroup )? undefined: 'all' 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 : (
toggleLockedPosition(doc)}>
); const paint = () => !doc?.onPaint ? null : (
togglePaintView(e, doc, props)}>
); 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 ? null : (
} closeOnSelect setSelectedVal={((dv: DocumentView) => { dv.select(false); SnappingManager.SetPropertiesWidth(250); _filterOpener?.(); }) as any // Dropdown assumes values are strings or numbers.. } size={Size.XSMALL} width={15} height={15} title={showFilterIcon === 'green' ? "This view is filtered. Click to view/change filters": "this view inherits filters from one of its parents"} color={SnappingManager.userColor} background={showFilterIcon} items={[ ...(dashView ? [dashView]: []), ...(docViewPath?.()??[])] .filter(dv => StrListCast(dv?.Document.childFilters).length || StrListCast(dv?.Document.childRangeFilters).length) .map(dv => ({ text: StrCast(dv?.Document.title), val: dv as any, style: {color:SnappingManager.userColor, background:SnappingManager.userBackgroundColor}, } as IListItemProps)) } />
); }; 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 ( {StrListCast(doc[fieldKey + 'audioAnnotations_text']).lastElement()}}>
DocumentView.getFirstDocumentView(doc)?.playAnnotation()}>
); }; const keywords = () => { if (doc && doc![DocData].showLabels && (!doc[DocData].data_labels || (doc[DocData].data_labels as List).length === 0)){ return () } else if (doc && doc![DocData].data_labels && doc![DocData].showLabels) { return () } } const iconTags = () => { if (doc && doc![DocData].showIconTags) {return ()} } return ( <> {paint()} {lock()} {filter()} {audio()} {keywords()} {iconTags()} ); } default: } } export function DashboardToggleButton(doc: Doc, field: string, onIcon: IconProp, offIcon: IconProp, clickFunc?: () => void) { const color = SnappingManager.userColor; return ( } onClick={undoBatch( action((e: React.MouseEvent) => { e.stopPropagation(); clickFunc ? clickFunc() : (doc[field] = doc[field] ? undefined : true); }) )} /> ); } /** * add hide button decorations for the "Dashboards" flyout TreeView */ export function DashboardStyleProvider(doc: Opt, props: Opt, property: string) { if (doc && property.split(':')[0] === StyleProp.Decorations) { return doc._type_collection === CollectionViewType.Docking || Doc.IsSystem(doc) ? null : DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => DocumentView.FocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.embedContainer ?? doc?.annotationOn))); } return DefaultStyleProvider(doc, props, property); } export function returnEmptyDocViewList() { return emptyPath; }