import { IconLookup, IconProp } from '@fortawesome/fontawesome-svg-core'; import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, Tooltip } from '@mui/material'; import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Toggle, ToggleType, Type } from 'browndash-components'; import { concat } from 'lodash'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorResult, SketchPicker } from 'react-color'; import * as Icons from 'react-icons/bs'; // {BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs" 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 { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; import { ComputedField } from '../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../fields/util'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { GroupManager } from '../util/GroupManager'; import { LinkManager } from '../util/LinkManager'; import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; import { Transform } from '../util/Transform'; import { UndoManager, undoBatch, undoable } from '../util/UndoManager'; import { EditableView } from './EditableView'; import { FilterPanel } from './FilterPanel'; import { InkStrokeProperties } from './InkStrokeProperties'; import { ObservableReactComponent } from './ObservableReactComponent'; import { PropertiesButtons } from './PropertiesButtons'; import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector'; import { PropertiesDocContextSelector } from './PropertiesDocContextSelector'; import { PropertiesSection } from './PropertiesSection'; import './PropertiesView.scss'; import { DefaultStyleProvider, SetFilterOpener as SetPropertiesFilterOpener, returnEmptyDocViewList } from './StyleProvider'; import { DocumentView } from './nodes/DocumentView'; import { StyleProviderFuncType } from './nodes/FieldView'; import { OpenWhere } from './nodes/OpenWhere'; import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; import { InkingStroke } from './InkingStroke'; import { SettingsManager } from '../util/SettingsManager'; import { MarqueeOptionsMenu } from './collections/collectionFreeForm'; import { SmartDrawHandler } from './smartdraw/SmartDrawHandler'; interface PropertiesViewProps { width: number; height: number; styleProvider?: StyleProviderFuncType; addDocTab: (doc: Doc, where: OpenWhere) => boolean; } @observer export class PropertiesView extends ObservableReactComponent { private _widthUndo?: UndoManager.Batch; // eslint-disable-next-line no-use-before-define public static Instance: PropertiesView; constructor(props: PropertiesViewProps) { super(props); makeObservable(this); PropertiesView.Instance = this; SetPropertiesFilterOpener( action(() => { this.CloseAll(); this.openFilters = true; }) ); } @computed get MAX_EMBED_HEIGHT() { return 200; } @computed get selectedDoc() { return DocumentView.SelectedSchemaDoc() || this.selectedDocumentView?.Document || Doc.ActiveDashboard; } @computed get selectedLink() { return LinkManager.Instance.currentLink; } @computed get selectedLayoutDoc() { return DocumentView.SelectedSchemaDoc() || this.selectedDocumentView?.layoutDoc || Doc.ActiveDashboard; } @computed get selectedDocumentView() { return DocumentView.Selected().lastElement(); } @computed get isPres(): boolean { return this.selectedDoc?.type === DocumentType.PRES; } @computed get dataDoc() { return this.selectedDoc?.[DocData]; } @observable layoutFields: boolean = false; @observable layoutDocAcls: boolean = false; @observable openOptions: boolean = true; @observable openSharing: boolean = true; @observable openFields: boolean = false; @observable openLayout: boolean = false; @observable openContexts: boolean = true; @observable openLinks: boolean = true; @observable openAppearance: boolean = true; @observable openTransform: boolean = true; @observable openFilters: boolean = false; @observable openStyling: boolean = true; // Pres Trails booleans: @observable openPresTransitions: boolean = true; @observable openPresProgressivize: boolean = false; @observable openPresVisibilityAndDuration: boolean = false; @observable openAddSlide: boolean = false; @observable openSlideOptions: boolean = false; // For ink groups @observable containsInkDoc: boolean = false; @observable inkDoc: Doc | undefined = undefined; @observable _controlButton: boolean = false; private _disposers: { [name: string]: IReactionDisposer } = {}; componentDidMount() { this._disposers.link = reaction( () => this.selectedLink, link => { link && this.CloseAll(); link && (this.openLinks = true); }, { fireImmediately: true } ); } componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); } @computed get isInk() { return this.selectedDoc?.type === DocumentType.INK; } @computed get isGroup() { return this.selectedDoc?.isGroup; } @computed get isStack() { return [CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Stacking, CollectionViewType.NoteTaking].includes(this.selectedDoc?.type_collection as CollectionViewType); } rtfWidth = () => (!this.selectedLayoutDoc ? 0 : Math.min(NumCast(this.selectedLayoutDoc?._width), this._props.width - 20)); rtfHeight = () => (!this.selectedLayoutDoc ? 0 : this.rtfWidth() <= NumCast(this.selectedLayoutDoc?._width) ? Math.min(NumCast(this.selectedLayoutDoc?._height), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT); @action docWidth = () => { const layoutDoc = this.selectedLayoutDoc; if (layoutDoc) { const aspect = Doc.NativeAspect(layoutDoc, undefined, !layoutDoc._layout_fitWidth); if (aspect) return Math.min(NumCast(layoutDoc._width), Math.min(this.MAX_EMBED_HEIGHT * aspect, this._props.width - 20)); return Doc.NativeWidth(layoutDoc) ? Math.min(NumCast(layoutDoc._width), this._props.width - 20) : this._props.width - 20; } return 0; }; @action docHeight = () => { const layoutDoc = this.selectedLayoutDoc; if (layoutDoc && this.dataDoc) { return Math.max( 70, Math.min( this.MAX_EMBED_HEIGHT, Doc.NativeAspect(layoutDoc, undefined, true) ? this.docWidth() / Doc.NativeAspect(layoutDoc, undefined, true) : layoutDoc._layout_fitWidth ? !Doc.NativeHeight(this.dataDoc) ? NumCast(this._props.height) : Math.min((this.docWidth() * Doc.NativeHeight(layoutDoc)) / Doc.NativeWidth(layoutDoc) || NumCast(this._props.height)) : NumCast(layoutDoc._height) || 50 ) ); } return 0; }; editableFields = (filter: (key: string) => boolean, reqdKeys: string[]) => { const rows: JSX.Element[] = []; if (this.dataDoc && this.selectedDoc) { const ids = new Set(reqdKeys); const docs: Doc[] = DocumentView.Selected().length < 2 // ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : DocumentView.Selected().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); docs.forEach(doc => Object.keys(doc) .filter(filter) .forEach(key => doc[key] !== ComputedField.undefined && key && ids.add(key)) ); Array.from(ids) .sort() .forEach(key => { const multiple = Array.from(docs.reduce((set, doc) => set.add(doc[key]), new Set()).keys()).length > 1; const editableContents = multiple ? '-multiple-' : Field.toKeyValueString(docs[0], key); const displayContents = multiple ? '-multiple-' : Field.toString(docs[0][key] as FieldType); const contentElement = ( editableContents} SetValue={(value: string) => { value !== '-multiple-' && docs.map(doc => Doc.SetField(doc, key, value, true)); return true; }} /> ); rows.push(
{key + ':'}   {contentElement}
); }); rows.push(
); } return rows; }; @computed get expandedField() { return this.editableFields(returnTrue, []); } @computed get noviceFields() { const noviceReqFields = ['author', 'author_date', 'tags', '_layout_curPage']; return this.editableFields(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('acl_')), noviceReqFields); } @undoBatch setKeyValue = (value: string) => { const docs = DocumentView.Selected().length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) // : this.dataDoc!] : DocumentView.Selected().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); // prettier-ignore docs.forEach(doc => { if (value.indexOf(':') !== -1) { const newVal = value[0].toUpperCase() + value.substring(1, value.length); const splits = newVal.split(':'); Doc.SetField(doc, splits[0], splits[1], true); const tags = StrCast(doc.tags, ':'); if (tags.includes(`${splits[0]}:`) && splits[1] === 'undefined') { Doc.SetField(doc, 'tags', `"${tags.replace(splits[0] + ':', '')}"`, true); } return true; } if (value[0] === '#') { const tags = StrListCast(doc.tags); if (!tags.includes(value)) { tags.push(value); doc[DocData].tags = tags.length ? new List(tags) : undefined; } return true; } return undefined; }); return false; }; @observable transform: Transform = Transform.Identity(); getTransform = () => this.transform; propertiesDocViewRef = (ref: HTMLDivElement) => { const resizeObserver = new ResizeObserver( action(() => { const cliRect = ref.getBoundingClientRect(); this.transform = new Transform(-cliRect.x, -cliRect.y, 1); }) ); ref && resizeObserver.observe(ref); }; @computed get contexts() { return !this.selectedDoc ? null : ; } @computed get contextCount() { if (this.selectedDocumentView) { const target = this.selectedDocumentView.Document; return Doc.GetEmbeddings(target).length - 1; } return 0; } @computed get links() { const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor ?? this.selectedDoc; return !selAnchor ? null : ; } @computed get linkCount() { const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor ?? this.selectedDoc; let counter = 0; Doc.Links(selAnchor).forEach(() => counter++); return counter; } @computed get layoutPreview() { if (DocumentView.Selected().length > 1) { return '-- multiple selected --'; } if (this.selectedDoc) { const layoutDoc = Doc.Layout(this.selectedDoc); 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 (
); } return null; } /** * Handles the changing of a user's permissions from the permissions panel. */ @undoBatch changePermissions = (e: React.ChangeEvent, user: string) => { const docs = DocumentView.Selected().length < 2 ? [this.selectedDoc] : DocumentView.Selected().map(dv => (this.layoutDocAcls ? dv.layoutDoc : dv.dataDoc)); SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs, this.layoutDocAcls); }; /** * @returns the options for the permissions dropdown. */ getPermissionsSelect(user: string, permission: string, showGuestOptions: boolean) { const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); if (permission === '-multiple-') dropdownValues.unshift(permission); return ( ); } /** * @returns the notification icon. On clicking, it should notify someone of a document been shared with them. */ @computed get notifyIcon() { return ( Notify with message}>
); } /** * ... next to the owner that opens the main SharingManager interface on click. */ @computed get expansionIcon() { return (
} size={Size.XSMALL} color={SnappingManager.userColor} onClick={action(() => { if (this.selectedDocumentView || this.selectedDoc) { SharingManager.Instance.open(this.selectedDocumentView?.Document === this.selectedDoc ? this.selectedDocumentView : undefined, this.selectedDoc); } })} />
); } /** * @returns a row of the permissions panel */ sharingItem(nameIn: string, admin: boolean, permission: string, showExpansionIcon?: boolean) { const name = nameIn === ClientUtils.CurrentUserEmail() ? 'Me' : nameIn; return (
this.selectedUser = this.selectedUser === name ? "" : name)} >
{' '} {name}{' '}
{/* {name !== "Me" ? this.notifyIcon : null} */}
{this.colorACLDropDown(name, admin, permission, false)} {(permission === 'Owner' && name === 'Me') || showExpansionIcon ? this.expansionIcon : null}
); } /** * @returns a colored dropdown bar reflective of the permission */ colorACLDropDown(name: string, admin: boolean, permission: string, showGuestOptions: boolean) { const shareImage = ReverseHierarchyMap.get(permission)?.image; return (
{admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission, showGuestOptions) : concat(shareImage, ' ', permission)}
); } /** * Sorting algorithm to sort users. */ sortUsers = (u1: string, u2: string) => (u1 > u2 ? -1 : u1 === u2 ? 0 : 1); /** * Sorting algorithm to sort groups. */ sortGroups = (group1: Doc, group2: Doc) => { const g1 = StrCast(group1.title); const g2 = StrCast(group2.title); return g1 > g2 ? -1 : g1 === g2 ? 0 : 1; }; /** * @returns the sharing and permissions panel. */ @computed get sharingTable() { // all selected docs const docs = DocumentView.Selected().length < 2 && this.selectedDoc ? [this.selectedDoc] : DocumentView.SelectedDocs(); const target = docs[0]; const showAdmin = GetEffectiveAcl(target) === AclAdmin; const individualTableEntries = []; const usersAdded: string[] = []; // all shared users being added - organized by denormalized email const seldoc = this.layoutDocAcls ? this.selectedLayoutDoc : this.selectedDoc?.[DocData]; // adds each user to usersAdded SharingManager.Instance.users.forEach(eachUser => { let userOnDoc = true; if (seldoc) { if (Doc.GetT(seldoc, 'acl_' + normalizeEmail(eachUser.user.email), 'string', true) === '' || Doc.GetT(seldoc, 'acl_' + normalizeEmail(eachUser.user.email), 'string', true) === undefined) { userOnDoc = false; } } if (userOnDoc && !usersAdded.includes(eachUser.user.email) && eachUser.user.email !== 'guest' && eachUser.user.email !== target.author) { usersAdded.push(eachUser.user.email); } }); // sorts and then adds each user to the table usersAdded.sort(this.sortUsers); usersAdded.forEach(userEmail => { const userKey = `acl_${normalizeEmail(userEmail)}`; const aclField = Doc.GetT(this.layoutDocAcls ? target : Doc.GetProto(target), userKey, 'string', true); const permission = StrCast(aclField); individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user }); // adds current user let userEmail = ClientUtils.CurrentUserEmail(); if (userEmail === 'guest') userEmail = 'Guest'; const userKey = `acl_${normalizeEmail(userEmail)}`; if (!usersAdded.includes(userEmail) && userEmail !== 'Guest' && userEmail !== target.author) { let permission; if (this.layoutDocAcls) { if (target[DocAcl][userKey]) permission = HierarchyMapping.get(target[DocAcl][userKey])?.name; else if (target.embedContainer) permission = StrCast(Doc.GetProto(DocCast(target.embedContainer))[userKey]); else permission = StrCast(Doc.GetProto(target)?.[userKey]); } else permission = StrCast(target[userKey]); individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user } // shift owner to top individualTableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner'), false); // adds groups const groupTableEntries: JSX.Element[] = []; const groupList = GroupManager.Instance?.allGroups || []; groupList.sort(this.sortGroups); groupList.forEach(group => { if (group.title !== 'Guest' && this.selectedDoc) { const groupKey = 'acl_' + normalizeEmail(StrCast(group.title)); if (this.selectedDoc[groupKey] !== '' && this.selectedDoc[groupKey] !== undefined) { let permission; if (this.layoutDocAcls) { if (target[DocAcl][groupKey]) { permission = HierarchyMapping.get(target[DocAcl][groupKey])?.name; } else if (target.embedContainer) permission = StrCast(Doc.GetProto(DocCast(target.embedContainer))[groupKey]); else permission = StrCast(Doc.GetProto(target)?.[groupKey]); } else permission = StrCast(target[groupKey]); groupTableEntries.unshift(this.sharingItem(StrCast(group.title), showAdmin, permission!, false)); } } }); // guest permission const guestPermission = StrCast((this.layoutDocAcls ? target : Doc.GetProto(target)).acl_Guest); return (

Individuals with Access to this Document
{individualTableEntries}
{groupTableEntries.length > 0 ? (

Groups with Access to this Document
{groupTableEntries}
) : null}
Guest
{this.colorACLDropDown('Guest', showAdmin, guestPermission!, true)}
); } @computed get fieldsCheckbox() { // color= "primary" return ; } @action toggleCheckbox = () => { this.layoutFields = !this.layoutFields; }; @computed get color() { return SnappingManager.userColor; } @computed get backgroundColor() { return SnappingManager.userBackgroundColor; } @computed get variantColor() { return SnappingManager.userVariantColor; } @computed get editableTitle() { const titles = new Set(); DocumentView.Selected().forEach(dv => titles.add(StrCast(dv.Document.title))); const title = Array.from(titles.keys()).length > 1 ? '--multiple selected--' : StrCast(this.selectedDoc?.title); return (
{LinkManager.Instance.currentLinkAnchor ? (

Anchor: {StrCast(LinkManager.Instance.currentLinkAnchor.title)}

) : null} {this.selectedLink?.title ? (

Link: {StrCast(this.selectedLink.title)}

) : null}
); } @computed get currentType() { const docType = StrCast(this.selectedDoc?.type) as DocumentType; const colType = StrCast(this.selectedDoc?.type_collection) as CollectionViewType; const capitalizedDocType = ClientUtils.cleanDocumentType(docType, colType); return (
Type {/*
Type
*/}
{this.currentComponent}
{capitalizedDocType}
); } @computed get currentComponent() { const iconName = StrCast(this.selectedDoc?.systemIcon); if (iconName) { const Icon = Icons[iconName as keyof typeof Icons]; return ; } return ; } @undoBatch setTitle = (value: string | number) => { if (DocumentView.Selected().length > 1) { DocumentView.Selected().map(dv => Doc.SetInPlace(dv.Document, 'title', value, true)); } else if (this.dataDoc) { if (this.selectedDoc) Doc.SetInPlace(this.selectedDoc, 'title', value, true); else Doc.SetField(this.dataDoc, 'title', value as string, true); } }; @undoBatch rotate = (angle: number) => { const _centerPoints: { X: number; Y: number }[] = []; const doc = this.selectedDoc; const layout = this.selectedLayoutDoc; if (doc && layout) { if (doc.type === DocumentType.INK && doc.x && doc.y && layout._width && layout._height && doc.data) { const ink = Cast(doc.stroke, InkField)?.inkData; if (ink) { const xs = ink.map(p => p.X); const ys = ink.map(p => p.Y); const left = Math.min(...xs); const top = Math.min(...ys); _centerPoints.push({ X: left, Y: top }); } } let index = 0; if (doc.type === DocumentType.INK && doc.x && doc.y && layout._width && layout._height && doc.data) { layout.rotation = NumCast(layout.rotation) + angle; const inks = Cast(doc.stroke, InkField)?.inkData; if (inks) { const newPoints: { X: number; Y: number }[] = []; inks.forEach(ink => { const newX = Math.cos(angle) * (ink.X - _centerPoints[index].X) - Math.sin(angle) * (ink.Y - _centerPoints[index].Y) + _centerPoints[index].X; const newY = Math.sin(angle) * (ink.X - _centerPoints[index].X) + Math.cos(angle) * (ink.Y - _centerPoints[index].Y) + _centerPoints[index].Y; newPoints.push({ X: newX, Y: newY }); }); doc.stroke = new InkField(newPoints); const xs = newPoints.map(p => p.X); const ys = newPoints.map(p => p.Y); const left = Math.min(...xs); const top = Math.min(...ys); const right = Math.max(...xs); const bottom = Math.max(...ys); layout._height = bottom - top; layout._width = right - left; } index++; } } }; @computed get controlPointsButton() { return (
Edit points
}>
{ InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton; })}>
); } inputBox = (key: string, value: string | number | undefined, setter: (val: string) => void, title: string) => (
{title}
setter(e.target.value)} onKeyDown={e => e.stopPropagation()} />
this.upDownButtons('up', key)), 'down btn' )}>
this.upDownButtons('down', key)), 'up btn' )}>
); inputBoxDuo = (key: string, value: string | number | undefined, setter: (val: string) => void, title1: string, key2: string, value2: string | number | undefined, setter2: (val: string) => void, title2: string) => (
{this.inputBox(key, value, setter, title1)} {title2 === '' ? null : this.inputBox(key2, value2, setter2, title2)}
); @action upDownButtons = (dirs: string, field: string) => { const selDoc = this.selectedDoc; if (!selDoc) return; // prettier-ignore switch (field) { case 'Xps': selDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10); break; case 'Yps': selDoc.y = NumCast(this.selectedDoc?.y) + (dirs === 'up' ? 10 : -10); break; case 'stk': selDoc.stroke_width = NumCast(this.selectedDoc?.[DocData].stroke_width) + (dirs === 'up' ? 0.1 : -0.1); break; case 'wid': { const oldWidth = NumCast(selDoc._width); const oldHeight = NumCast(selDoc._height); const oldX = NumCast(selDoc.x); const oldY = NumCast(selDoc.y); selDoc._width = oldWidth + (dirs === 'up' ? 10 : -10); if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) { const ink = Cast(selDoc.data, InkField)?.inkData; if (ink) { const newPoints: { X: number; Y: number }[] = []; for (let j = 0; j < ink.length; j++) { // (new x — oldx) + (oldxpoint * newWidt)/oldWidth const newX = NumCast(selDoc.x) - oldX + (ink[j].X * NumCast(selDoc._width)) / oldWidth; const newY = NumCast(selDoc.y) - oldY + (ink[j].Y * NumCast(selDoc._height)) / oldHeight; newPoints.push({ X: newX, Y: newY }); } selDoc.data = new InkField(newPoints); } } } break; case 'hgt': { const oWidth = NumCast(selDoc._width); const oHeight = NumCast(selDoc._height); const oX = NumCast(selDoc.x); const oY = NumCast(selDoc.y); selDoc._height = oHeight + (dirs === 'up' ? 10 : -10); if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) { const ink = Cast(selDoc.data, InkField)?.inkData; if (ink) { const newPoints: { X: number; Y: number }[] = []; for (let j = 0; j < ink.length; j++) { // (new x — oldx) + (oldxpoint * newWidt)/oldWidth const newX = NumCast(selDoc.x) - oX + (ink[j].X * NumCast(selDoc._width)) / oWidth; const newY = NumCast(selDoc.y) - oY + (ink[j].Y * NumCast(selDoc._height)) / oHeight; newPoints.push({ X: newX, Y: newY }); } selDoc.data = new InkField(newPoints); } } } break; default: { /* empty */ } } }; getField(key: string) { return Field.toString(this.selectedDoc?.[DocData][key] as FieldType); } @computed get selectedStrokes() { return this.containsInkDoc ? DocListCast(this.selectedDoc[DocData].data) : this.selectedDoc ? [this.selectedDoc] : []; } @computed get shapeXps() { return NumCast(this.selectedDoc?.x); } // prettier-ignore set shapeXps(value) { this.selectedDoc && (this.selectedDoc.x = Math.round(value * 100) / 100); } // prettier-ignore @computed get shapeYps() { return NumCast(this.selectedDoc?.y); } // prettier-ignore set shapeYps(value) { this.selectedDoc && (this.selectedDoc.y = Math.round(value * 100) / 100); } // prettier-ignore @computed get shapeWid() { return NumCast(this.selectedDoc?._width); } // prettier-ignore set shapeWid(value) { this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100); } // prettier-ignore @computed get shapeHgt() { return NumCast(this.selectedDoc?._height); } // prettier-ignore set shapeHgt(value) { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } // prettier-ignore @computed get strokeThk(){ return this.containsInkDoc ? NumCast(this.inkDoc?.[DocData].stroke_width) : NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore set strokeThk(value) { this.selectedStrokes.forEach(doc => { doc[DocData].stroke_width = Math.round(value * 100) / 100; }); } @computed get hgtInput() { return this.inputBoxDuo( 'hgt', this.shapeHgt, undoable((val: string) => { !Number(val) && (this.shapeHgt = +val); }, 'set height'), 'H:', 'wid', this.shapeWid, undoable((val: string) => { !isNaN(Number(val)) && (this.shapeWid = +val); }, 'set width'), 'W:' ); } @computed get XpsInput() { // prettier-ignore return this.inputBoxDuo( 'Xps', this.shapeXps, undoable((val: string) => { val !== '0' && !isNaN(Number(val)) && (this.shapeXps = +val); }, 'set x coord'), 'X:', 'Yps', this.shapeYps, undoable((val: string) => { val !== '0' && !isNaN(Number(val)) && (this.shapeYps = +val); }, 'set y coord'), 'Y:' ); } @observable private _fillBtn = false; @observable private _lineBtn = false; private _lastDash: string = '2'; @computed get colorFil() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].fillColor) : StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore set colorFil(value) { this.selectedStrokes.forEach(doc => { const inkStroke = DocumentView.getDocumentView(doc)?.ComponentView as InkingStroke; const { inkData } = inkStroke.inkScaledData(); if (InkingStroke.IsClosed(inkData)) { doc[DocData].fillColor = value || undefined; } }); } @computed get colorStk() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].color) : StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore set colorStk(value) { this.selectedStrokes.forEach(doc => { doc[DocData].color = value || undefined; }); } colorButton(value: string, type: string, setter: () => void) { return (
setter())}>
{value === '' || value === 'transparent' ?

: ''}
); } colorPicker(color: string, setter: (color: string) => void) { return ( setter(col.hex)), 'set stroke color property' )} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} color={color} /> ); } @computed get fillButton() { return this.colorButton(this.colorFil, 'fill', () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; }); } @computed get lineButton() { return this.colorButton(this.colorStk, 'line', () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; }); } @computed get fillPicker() { return this.colorPicker(this.colorFil, (color: string) => { this.colorFil = color; }); // prettier-ignore } @computed get linePicker() { return this.colorPicker(this.colorStk, (color: string) => { this.colorStk = color; }); // prettier-ignore } @computed get strokeAndFill() { return (
Fill:
{this.fillButton}
Stroke:
{this.lineButton}
{this._fillBtn ? this.fillPicker : ''} {this._lineBtn ? this.linePicker : ''}
); } @computed get smoothAndColor() { const targetDoc = this.selectedLayoutDoc; return (
{!targetDoc.layout_isSvg && (
} iconPlacement="left" align="flex-start" fillWidth toggleType={ToggleType.BUTTON} onClick={undoable(() => { SmartDrawHandler.Instance.colorWithGPT(targetDoc); }, 'smoothStrokes')} />
)}
} iconPlacement="left" align="flex-start" fillWidth toggleType={ToggleType.BUTTON} onClick={undoable(() => { InkStrokeProperties.Instance.smoothInkStrokes(this.containsInkDoc ? DocListCast(targetDoc.data) : [targetDoc], this.smoothAmt); }, 'smoothStrokes')} />
{this.getNumber( 'Smooth Amount', '', 1, Math.max(10, this.smoothAmt), this.smoothAmt, (val: number) => { !isNaN(val) && (this.smoothAmt = val); }, 10, 1 )}
); } @computed get dashdStk() { return this.containsInkDoc? this.inkDoc?.stroke_dash || '' : this.selectedDoc?.stroke_dash || ''; } // prettier-ignore set dashdStk(value) { value && (this._lastDash = value as string); this.selectedStrokes.forEach(doc => { doc[DocData].stroke_dash = value ? this._lastDash : undefined; }); } @computed get widthStk() { return this.getField('stroke_width') || '1'; } // prettier-ignore set widthStk(value) { this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Number(value)); } @computed get markScal() { return Number(this.getField('stroke_markerScale') || '1'); } // prettier-ignore set markScal(value) { this.selectedStrokes.forEach(doc => { doc[DocData].stroke_markerScale = Number(value); }); } @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '5'); } // prettier-ignore set smoothAmt(value) { this.selectedDoc && (this.selectedDoc[DocData].stroke_smoothAmount = Number(value)); } @computed get markHead() { return this.getField('stroke_startMarker') || ''; } // prettier-ignore set markHead(value) { this.selectedStrokes.forEach(doc => { doc[DocData].stroke_startMarker = value; }); } @computed get markTail() { return this.getField('stroke_endMarker') || ''; } // prettier-ignore set markTail(value) { this.selectedStrokes.forEach(doc => { doc[DocData].stroke_endMarker = value; }); } regInput = (key: string, value: string | number | undefined, setter: (val: string) => void) => (
setter(e.target.value)} />
this.upDownButtons('up', key)), 'up' )}>
this.upDownButtons('down', key)), 'down' )}>
); @action CloseAll = () => { this.openContexts = false; this.openLinks = false; this.openOptions = false; this.openTransform = false; this.openFields = false; this.openSharing = false; this.openLayout = false; this.openFilters = false; }; @computed get widthAndDash() { return ( // prettier-ignore
{this.getNumber( 'Thickness', '', 0, Math.max(50, this.strokeThk), this.strokeThk, (val: number) => { !isNaN(val) && (this.strokeThk = val); }, 50, 1 )}
{this.getNumber( 'Arrow Scale', '', 0, Math.max(10, this.markScal), this.markScal, (val: number) => { !isNaN(val) && (this.markScal = val); }, 10, 1 )}
Arrow Head:
{ this.markHead = this.markHead ? '' : 'arrow'; }), "change arrow head")} />
Arrow End:
{ this.markTail = this.markTail ? '' : 'arrow'; }) ,"change arrow tail" )} />
Dashed Line:
); } @undoBatch changeDash = () => { this.dashdStk = this.dashdStk === '2' ? '0' : '2'; }; @computed get appearanceEditor() { return (
{this.widthAndDash} {this.strokeAndFill} {this.smoothAndColor}
); } @computed get inkEditor() { return (
{this.widthAndDash} {this.strokeAndFill}
); } _sliderBatch: UndoManager.Batch | undefined; setFinalNumber = () => { this._sliderBatch?.end(); }; getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: (val: number) => void, autorange?: number, autorangeMinVal?: number) => (
{ this._sliderBatch = UndoManager.StartBatch('slider ' + label); }} multithumb={false} color={this.color} size={Size.XSMALL} min={min} max={max} autorangeMinVal={autorangeMinVal} autorange={autorange} number={number} unit={unit} decimals={1} setFinalNumber={this.setFinalNumber} setNumber={setNumber} fillWidth />
); setVal = (func: (doc: Doc, val: number) => void) => (val: number) => this.selectedDoc && !isNaN(val) && func(this.selectedDoc, val); @computed get transformEditor() { return ( // prettier-ignore
{!this.isStack ? null : this.getNumber('Gap', ' px', 0, 200, NumCast(this.selectedDoc!.gridGap), this.setVal((doc: Doc, val: number) => { doc.gridGap = val; })) } {!this.isStack ? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), this.setVal((doc: Doc, val: number) => { doc.xMargin = val; })) } {!this.isStack ? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), this.setVal((doc: Doc, val: number) => { doc.yMargin = val; })) } {!this.isGroup ? null : this.getNumber('Padding', ' px', 0, 500, NumCast(this.selectedDoc!.xPadding), this.setVal((doc: Doc, val: number) => { doc.xPadding = doc.yPadding = val; })) } {this.isInk ? this.controlPointsButton : null} {this.getNumber('Width', ' px', 0, Math.max(1000, this.shapeWid), this.shapeWid, this.setVal((doc: Doc, val:number) => {this.shapeWid = val}), 1000, 1)} {this.getNumber('Height', ' px', 0, Math.max(1000, this.shapeHgt), this.shapeHgt, this.setVal((doc: Doc, val:number) => {this.shapeHgt = val}), 1000, 1)} {this.getNumber('X', ' px', this.shapeXps - 500, this.shapeXps + 500, this.shapeXps, this.setVal((doc: Doc, val:number) => {this.shapeXps = val}), 1000)} {this.getNumber('Y', ' px', this.shapeYps - 500, this.shapeYps + 500, this.shapeYps, this.setVal((doc: Doc, val:number) => {this.shapeYps = val}), 1000)}
); } @computed get optionsSubMenu() { return ( // prettier-ignore { this.openOptions = bool; }} onDoubleClick={this.CloseAll}> ); } @computed get sharingSubMenu() { return ( // prettier-ignore { this.openSharing = bool; }} onDoubleClick={this.CloseAll}> <> {/*
*/}
Layout Permissions { this.layoutDocAcls = !this.layoutDocAcls; })} checked={this.layoutDocAcls} />
{/*
{"Re-distribute sharing settings"}
}>
*/} {/*
*/} {this.sharingTable}
); } /** * Updates this.filterDoc's currentFilter and saves the childFilters on the currentFilter */ updateFilterDoc = (doc: Doc) => { if (this.selectedDoc) { if (doc === this.selectedDoc.currentFilter) return; // causes problems if you try to reapply the same doc const savedDocFilters = doc._childFiltersList; const currentDocFilters = this.selectedDoc._childFilters; this.selectedDoc._childFilters = new List(); (this.selectedDoc.currentFilter as Doc)._childFiltersList = currentDocFilters; this.selectedDoc.currentFilter = doc; doc._childFiltersList = new List(); this.selectedDoc._childFilters = savedDocFilters; const savedDocRangeFilters = doc._childFiltersByRangesList; const currentDocRangeFilters = this.selectedDoc._childFiltersByRanges; this.selectedDoc._childFiltersByRanges = new List(); (this.selectedDoc.currentFilter as Doc)._childFiltersByRangesList = currentDocRangeFilters; this.selectedDoc.currentFilter = doc; doc._childFiltersByRangesList = new List(); this.selectedDoc._childFiltersByRanges = savedDocRangeFilters; } }; @computed get filtersSubMenu() { return ( // prettier-ignore { this.openFilters = bool; })} onDoubleClick={this.CloseAll}>
); } @computed get inkSubMenu() { this.containsInkDoc = false; return ( // prettier-ignore <> { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}> {this.selectedLayoutDoc?.layout_isSvg ? this.appearanceEditor : null} { this.openTransform = bool; }} onDoubleClick={this.CloseAll}> {this.transformEditor} ); } /** * Determines if a selected collection/group document contains any ink strokes to allow users to edit groups * of ink strokes in the properties menu. */ containsInk = (selectedDoc: Doc) => { const childDocs: Doc[] = DocListCast(selectedDoc[DocData].data); for (var i = 0; i < childDocs.length; i++) { if (DocumentView.getDocumentView(childDocs[i])?.layoutDoc?.layout_isSvg) { this.inkDoc = childDocs[i]; this.containsInkDoc = true; return true; } } this.containsInkDoc = false; return false; }; @computed get inkCollectionSubMenu() { return ( // prettier-ignore <> { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}> {this.isGroup && this.containsInk(this.selectedDoc) ? this.appearanceEditor : null} ); } @computed get fieldsSubMenu() { return ( { this.openFields = bool; }} onDoubleClick={this.CloseAll}>
{Doc.noviceMode ? this.noviceFields : this.expandedField}
); } @computed get contextsSubMenu() { return ( { this.openContexts = bool; }} onDoubleClick={this.CloseAll}> {this.contextCount > 0 ? this.contexts : 'There are no other contexts.'} ); } @computed get linksSubMenu() { return ( { this.openLinks = bool; }} onDoubleClick={this.CloseAll}> {this.linkCount > 0 ? this.links : 'There are no current links.'} ); } @computed get layoutSubMenu() { return ( { this.openLayout = bool; }} onDoubleClick={this.CloseAll}> {this.layoutPreview} ); } @computed get description() { return Field.toString(this.selectedLink?.link_description as FieldType); } @computed get relationship() { return StrCast(this.selectedLink?.link_relationship); } @observable private relationshipButtonColor: string = ''; // @action // handleDescriptionChange = (e: React.ChangeEvent) => { this.link_description = e.target.value; } // handleRelationshipChange = (e: React.ChangeEvent) => { this.link_relationship = e.target.value; } handleDescriptionChange = undoable( action((value: string) => { if (this.selectedLink) { this.setDescripValue(value); } }), 'change link description' ); handlelinkRelationshipChange = undoable( action((value: string) => { if (this.selectedLink) { this.setlinkRelationshipValue(value); } }), 'change link relationship' ); @undoBatch setDescripValue = action((value: string) => { if (this.selectedLink) { this.selectedLink[DocData].link_description = value; } }); @undoBatch setlinkRelationshipValue = action((value: string) => { if (this.selectedLink) { const prevRelationship = StrCast(this.selectedLink.link_relationship); this.selectedLink.link_relationship = value; Doc.GetProto(this.selectedLink).link_relationship = value; const linkRelationshipList = StrListCast(Doc.UserDoc().link_relationshipList); const linkRelationshipSizes = NumListCast(Doc.UserDoc().link_relationshipSizes); const linkColorList = StrListCast(Doc.UserDoc().link_ColorList); // if the relationship does not exist in the list, add it and a corresponding unique randomly generated color if (!linkRelationshipList?.includes(value)) { linkRelationshipList.push(value); linkRelationshipSizes.push(1); const randColor = 'rgb(' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')'; linkColorList.push(randColor); // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes } else if (linkRelationshipList && value !== prevRelationship) { const index = linkRelationshipList.indexOf(value); // increment size of new relationship size if (index !== -1 && index < linkRelationshipSizes.length) { const pvalue = linkRelationshipSizes[index]; linkRelationshipSizes[index] = pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1; } // decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation) if (linkRelationshipList.includes(prevRelationship)) { const pindex = linkRelationshipList.indexOf(prevRelationship); if (pindex !== -1 && pindex < linkRelationshipSizes.length) { const pvalue = linkRelationshipSizes[pindex]; linkRelationshipSizes[pindex] = Math.max(0, pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue - 1); } } } this.relationshipButtonColor = 'rgb(62, 133, 55)'; setTimeout(action(() => { this.relationshipButtonColor = ''; }), 750); // prettier-ignore return true; } return undefined; }); changeFollowBehavior = undoable((loc: Opt) => { this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc); }, 'change follow behavior'); @undoBatch changeAnimationBehavior = action((behavior: string) => { this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior); }); @undoBatch changeEffectDirection = action((effect: PresEffectDirection) => { this.sourceAnchor && (this.sourceAnchor.followLinkAnimDirection = effect); }); animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => { const lanch = this.sourceAnchor; const color = lanch?.followLinkAnimDirection === direction || (direction === PresEffectDirection.Center && !lanch?.followLinkAnimDirection) ? Colors.MEDIUM_BLUE : ''; return ( {direction}
}>
this.changeEffectDirection(direction)}> {icon ? : null}
); }; onSelectOutDesc = () => { this.setDescripValue(this.description); document.getElementById('link_description_input')?.blur(); }; onDescriptionKey = () => { // if (e.key === 'Enter') { // this.setDescripValue(this.description); // document.getElementById('link_description_input')?.blur(); // } }; onSelectOutRelationship = () => { this.setlinkRelationshipValue(this.relationship); document.getElementById('link_relationship_input')?.blur(); }; onRelationshipKey = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { this.setlinkRelationshipValue(this.relationship); document.getElementById('link_relationship_input')?.blur(); } }; toggleLinkProp = (e: React.PointerEvent, prop: string) => { setupMoveUpEvents( this, e, returnFalse, emptyFunction, undoable(action(() => { this.selectedLink && (this.selectedLink[prop] = !this.selectedLink[prop]); }), `toggle prop: ${prop}`) // prettier-ignore ); }; @computed get destinationAnchor() { const ldoc = this.selectedLink; const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor; if (ldoc && lanch) return Doc.getOppositeAnchor(ldoc, lanch) ?? lanch; return ldoc ? DocCast(ldoc.link_anchor_2) : ldoc; } @computed get sourceAnchor() { const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor; return selAnchor ?? (this.selectedLink && this.destinationAnchor ? Doc.getOppositeAnchor(this.selectedLink, this.destinationAnchor) : this.selectedLink); } toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc, value: FieldType = true, ovalue: FieldType = false, cb: (val: FieldType) => void = val => val) => { anchor && setupMoveUpEvents( this, e, returnFalse, emptyFunction, undoable(action(() => { anchor[prop] = anchor[prop] === value ? ovalue : value; this.selectedDoc && cb(anchor[prop] as boolean); }), `toggle anchor prop: ${prop}`) // prettier-ignore ); }; @computed get editRelationship() { return ( this.handlelinkRelationshipChange(e.currentTarget.value)} className="text" type="text" /> ); } @computed get editDescription() { return (