import React = require('react'); import { IconLookup } from '@fortawesome/fontawesome-svg-core'; import { faAnchor, faArrowRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, Tooltip } from '@material-ui/core'; import { intersection } from 'lodash'; import { action, autorun, computed, Lambda, observable } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; import { ComputedField } from '../../fields/ScriptField'; import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { denormalizeEmail, GetEffectiveAcl, SharingPermissions } from '../../fields/util'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { DocumentManager } from '../util/DocumentManager'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; import { SharingManager } from '../util/SharingManager'; import { Transform } from '../util/Transform'; import { undoBatch, UndoManager } from '../util/UndoManager'; import { EditableView } from './EditableView'; import { Colors } from './global/globalEnums'; import { InkStrokeProperties } from './InkStrokeProperties'; import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView'; import { FilterBox } from './nodes/FilterBox'; import { KeyValueBox } from './nodes/KeyValueBox'; import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; import { PropertiesButtons } from './PropertiesButtons'; import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector'; import { PropertiesDocContextSelector } from './PropertiesDocContextSelector'; import './PropertiesView.scss'; import { DefaultStyleProvider } from './StyleProvider'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; const _global = (window /* browser */ || global) /* node */ as any; interface PropertiesViewProps { width: number; height: number; styleProvider?: StyleProviderFunc; addDocTab: (doc: Doc, where: OpenWhere) => boolean; } @observer export class PropertiesView extends React.Component { private _widthUndo?: UndoManager.Batch; @computed get MAX_EMBED_HEIGHT() { return 200; } @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc || Doc.ActiveDashboard; } @computed get selectedDocumentView() { if (SelectionManager.Views().length) return SelectionManager.Views()[0]; if (PresBox.Instance?.selectedArray.size) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); return undefined; } @computed get isPres(): boolean { return this.selectedDoc?.type === DocumentType.PRES; } @computed get isLink(): boolean { return this.selectedDoc?.type === DocumentType.LINK; } @computed get dataDoc() { return this.selectedDoc?.[DataSym]; } @observable layoutFields: boolean = false; @observable openOptions: boolean = true; @observable openSharing: boolean = true; @observable openFields: boolean = true; @observable openLayout: boolean = false; @observable openContexts: boolean = true; @observable openLinks: boolean = true; @observable openAppearance: boolean = true; @observable openTransform: boolean = true; @observable openFilters: boolean = true; // should be false /** * autorun to set up the filter doc of a collection if that collection has been selected and the filters panel is open */ private selectedDocListenerDisposer: Opt; // @observable selectedUser: string = ""; // @observable addButtonPressed: boolean = false; @observable layoutDocAcls: boolean = false; //Pres Trails booleans: @observable openPresTransitions: boolean = false; @observable openAddSlide: boolean = false; @observable openSlideOptions: boolean = false; @observable inOptions: boolean = false; @observable _controlButton: boolean = false; @observable _lock: boolean = false; componentDidMount() { this.selectedDocListenerDisposer?.(); this.selectedDocListenerDisposer = autorun(() => this.openFilters && this.selectedDoc && this.checkFilterDoc()); } componentWillUnmount() { this.selectedDocListenerDisposer?.(); } @computed get isInk() { return this.selectedDoc?.type === DocumentType.INK; } rtfWidth = () => (!this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20)); rtfHeight = () => (!this.selectedDoc ? 0 : this.rtfWidth() <= this.selectedDoc?.[WidthSym]() ? Math.min(this.selectedDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT); @action docWidth = () => { if (this.selectedDoc) { const layoutDoc = this.selectedDoc; const aspect = Doc.NativeAspect(layoutDoc, undefined, !layoutDoc._fitWidth); if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.width - 20)); return Doc.NativeWidth(layoutDoc) ? Math.min(layoutDoc[WidthSym](), this.props.width - 20) : this.props.width - 20; } else { return 0; } }; @action docHeight = () => { if (this.selectedDoc && this.dataDoc) { const layoutDoc = this.selectedDoc; return Math.max( 70, Math.min( this.MAX_EMBED_HEIGHT, Doc.NativeAspect(layoutDoc, undefined, true) ? this.docWidth() / Doc.NativeAspect(layoutDoc, undefined, true) : layoutDoc._fitWidth ? !Doc.NativeHeight(this.dataDoc) ? NumCast(this.props.height) : Math.min((this.docWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / Doc.NativeWidth(layoutDoc) || NumCast(this.props.height)) : NumCast(layoutDoc._height) || 50 ) ); } return 0; }; @computed get expandedField() { if (this.dataDoc && this.selectedDoc) { const ids: { [key: string]: string } = {}; const docs = SelectionManager.Views().length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); docs.forEach(doc => Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key))); const rows: JSX.Element[] = []; for (const key of Object.keys(ids).slice().sort()) { const docvals = new Set(); docs.forEach(doc => docvals.add(doc[key])); const contents = Array.from(docvals.keys()).length > 1 ? '-multiple' : docs[0][key]; if (key[0] === '#') { rows.push(
{key}  
); } else { const contentElement = ( (contents !== undefined ? Field.toString(contents as Field) : 'null')} SetValue={(value: string) => { docs.map(doc => KeyValueBox.SetField(doc, key, value, true)); return true; }} /> ); rows.push(
{key + ':'}   {contentElement}
); } } rows.push(
''} SetValue={this.setKeyValue} />
); return rows; } } @computed get noviceFields() { if (this.dataDoc) { const ids: { [key: string]: string } = {}; const docs = SelectionManager.Views().length < 2 ? [this.dataDoc] : SelectionManager.Views().map(dv => dv.dataDoc); docs.forEach(doc => Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key))); const rows: JSX.Element[] = []; const noviceReqFields = ['author', 'creationDate', 'tags']; const noviceLayoutFields = ['_curPage']; const noviceKeys = [...Array.from(Object.keys(ids)).filter(key => key[0] === '#' || key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('acl'))), ...noviceReqFields, ...noviceLayoutFields]; for (const key of noviceKeys.sort()) { const docvals = new Set(); docs.forEach(doc => docvals.add(doc[key])); const contents = Array.from(docvals.keys()).length > 1 ? '-multiple' : docs[0][key]; if (key[0] === '#') { rows.push(
{key}  
); } else if (contents !== undefined) { const value = Field.toString(contents as Field); if (noviceReqFields.includes(key) || key.indexOf('lastModified') !== -1) { rows.push(
{key + ': '}
{value}
); } else { const contentElement = ( (contents !== undefined ? Field.toString(contents as Field) : 'null')} SetValue={(value: string) => { docs.map(doc => KeyValueBox.SetField(doc, key, value, true)); return true; }} /> ); rows.push(
{key + ':'}   {contentElement}
); } } } rows.push(
''} SetValue={this.setKeyValue} />
); return rows; } } @undoBatch setKeyValue = (value: string) => { const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); docs.forEach(doc => { if (value.indexOf(':') !== -1) { const newVal = value[0].toUpperCase() + value.substring(1, value.length); const splits = newVal.split(':'); KeyValueBox.SetField(doc, splits[0], splits[1], true); const tags = StrCast(doc.tags, ':'); if (tags.includes(`${splits[0]}:`) && splits[1] === 'undefined') { KeyValueBox.SetField(doc, 'tags', `"${tags.replace(splits[0] + ':', '')}"`, true); } return true; } else if (value[0] === '#') { const newVal = value + `:'${value}'`; doc[DataSym][value] = value; const tags = StrCast(doc.tags, ':'); if (!tags.includes(`${value}:`)) { doc[DataSym].tags = `${tags + value + ':'}`; } return true; } }); return false; }; @observable transform: Transform = Transform.Identity(); getTransform = () => this.transform; propertiesDocViewRef = (ref: HTMLDivElement) => { const observer = new _global.ResizeObserver( action((entries: any) => { const cliRect = ref.getBoundingClientRect(); this.transform = new Transform(-cliRect.x, -cliRect.y, 1); }) ); ref && observer.observe(ref); }; @computed get contexts() { return !this.selectedDoc ? null : ; } @computed get links() { const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; return !selAnchor ? null : ; } @computed get layoutPreview() { if (SelectionManager.Views().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 (
); } else { return null; } } /** * Handles the changing of a user's permissions from the permissions panel. */ @undoBatch changePermissions = (e: any, user: string) => { const docs = SelectionManager.Views().length < 2 ? (this.selectedDoc ? [this.selectedDoc] : []) : SelectionManager.Views().map(docView => docView.props.Document); SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs); }; /** * @returns the options for the permissions dropdown. */ getPermissionsSelect(user: string, permission: string) { const dropdownValues: string[] = Object.values(SharingPermissions); if (permission === '-multiple-') dropdownValues.unshift(permission); if (user === 'Override') dropdownValues.unshift('None'); 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 ( Show more permissions}>
{ if (this.selectedDocumentView || this.selectedDoc) { SharingManager.Instance.open(this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView : undefined, this.selectedDoc); } }}>
); } /** * @returns a row of the permissions panel */ sharingItem(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) { return (
this.selectedUser = this.selectedUser === name ? "" : name)} >
{' '} {name}{' '}
{/* {name !== "Me" ? this.notifyIcon : null} */}
{admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission) : permission} {permission === 'Owner' || showExpansionIcon ? this.expansionIcon : null}
); } /** * @returns the sharing and permissions panel. */ @computed get sharingTable() { const AclMap = new Map([ [AclUnset, 'None'], [AclPrivate, SharingPermissions.None], [AclReadonly, SharingPermissions.View], [AclAugment, SharingPermissions.Augment], [AclSelfEdit, SharingPermissions.SelfEdit], [AclEdit, SharingPermissions.Edit], [AclAdmin, SharingPermissions.Admin], ]); // all selected docs const docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.selectedDoc : this.selectedDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym])); const target = docs[0]; // tslint:disable-next-line: no-unnecessary-callback-wrapper const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); const showAdmin = effectiveAcls.every(acl => acl === AclAdmin); // users in common between all docs const commonKeys: string[] = intersection(...docs.map(doc => (this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym][AclSym] && Object.keys(doc[DataSym][AclSym])))); const tableEntries = []; // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { if (commonKeys.length) { for (const key of commonKeys) { const name = denormalizeEmail(key.substring(4)); const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key] : doc?.[DataSym]?.[AclSym]?.[key] === docs[0]?.[DataSym]?.[AclSym]?.[key])); if (name !== Doc.CurrentUserEmail && name !== target.author && name !== 'Public' && name !== 'Override' /* && sidebarUsersDisplayed![name] !== false*/) { tableEntries.push(this.sharingItem(name, showAdmin, uniform ? AclMap.get(this.layoutDocAcls ? target[AclSym][key] : target[DataSym][AclSym][key])! : '-multiple-')); } } } const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); // shifts the current user, owner, public to the top of the doc. // tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); tableEntries.unshift(this.sharingItem('Public', showAdmin, docs.filter(doc => doc).every(doc => doc['acl-Public'] === docs[0]['acl-Public']) ? AclMap.get(target[AclSym]?.['acl-Public']) || SharingPermissions.None : '-multiple-')); tableEntries.unshift( this.sharingItem('Me', showAdmin, docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? 'Owner' : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? AclMap.get(effectiveAcls[0])! : '-multiple-', !ownerSame) ); if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner')); return
{tableEntries}
; } @computed get fieldsCheckbox() { return ; } @action toggleCheckbox = () => { this.layoutFields = !this.layoutFields; }; @computed get editableTitle() { const titles = new Set(); SelectionManager.Views().forEach(dv => titles.add(StrCast(dv.rootDoc.title))); const title = Array.from(titles.keys()).length > 1 ? '--multiple selected--' : StrCast(this.selectedDoc?.title); return (
title} SetValue={this.setTitle} />
); } @undoBatch @action setTitle = (value: string) => { if (SelectionManager.Views().length > 1) { SelectionManager.Views().map(dv => Doc.SetInPlace(dv.rootDoc, 'title', value, true)); return true; } else if (this.dataDoc) { if (this.selectedDoc) Doc.SetInPlace(this.selectedDoc, 'title', value, true); else KeyValueBox.SetField(this.dataDoc, 'title', value, true); return true; } return false; }; @undoBatch @action rotate = (angle: number) => { const _centerPoints: { X: number; Y: number }[] = []; if (this.selectedDoc) { const doc = this.selectedDoc; if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { const ink = Cast(doc.data, 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); const right = Math.max(...xs); const bottom = Math.max(...ys); _centerPoints.push({ X: left, Y: top }); } } var index = 0; if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { doc.rotation = NumCast(doc.rotation) + angle; const inks = Cast(doc.data, 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.data = 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); doc._height = bottom - top; doc._width = right - left; } index++; } } }; @computed get controlPointsButton() { return (
{'Edit points'}
}>
(InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton))}>
{InkStrokeProperties.Instance._lock ? 'Unlock ratio' : 'Lock ratio'}}>
(InkStrokeProperties.Instance._lock = !InkStrokeProperties.Instance._lock))}>
{'Rotate 90˚'}}>
this.rotate(Math.PI / 2))}>
); } inputBox = (key: string, value: any, setter: (val: string) => {}, title: string) => { return (
{title}
{ setter(e.target.value); }} onKeyPress={e => { e.stopPropagation(); }} />
this.upDownButtons('up', key)))}>
this.upDownButtons('down', key)))}>
); }; inputBoxDuo = (key: string, value: any, setter: (val: string) => {}, title1: string, key2: string, value2: any, setter2: (val: string) => {}, title2: string) => { return (
{this.inputBox(key, value, setter, title1)} {title2 === '' ? null : this.inputBox(key2, value2, setter2, title2)}
); }; @action upDownButtons = (dirs: string, field: string) => { switch (field) { case 'rot': this.rotate(dirs === 'up' ? 0.1 : -0.1); break; case 'Xps': this.selectedDoc && (this.selectedDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10)); break; case 'Yps': this.selectedDoc && (this.selectedDoc.y = NumCast(this.selectedDoc?.y) + (dirs === 'up' ? 10 : -10)); break; case 'stk': this.selectedDoc && (this.selectedDoc.strokeWidth = NumCast(this.selectedDoc?.strokeWidth) + (dirs === 'up' ? 0.1 : -0.1)); break; case 'wid': const oldWidth = NumCast(this.selectedDoc?._width); const oldHeight = NumCast(this.selectedDoc?._height); const oldX = NumCast(this.selectedDoc?.x); const oldY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === 'up' ? 10 : -10)); InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth) * NumCast(this.selectedDoc?._height)); const doc = this.selectedDoc; if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { const ink = Cast(doc.data, InkField)?.inkData; if (ink) { const newPoints: { X: number; Y: number }[] = []; for (var j = 0; j < ink.length; j++) { // (new x — oldx) + (oldxpoint * newWidt)/oldWidth const newX = NumCast(doc.x) - oldX + (ink[j].X * NumCast(doc._width)) / oldWidth; const newY = NumCast(doc.y) - oldY + (ink[j].Y * NumCast(doc._height)) / oldHeight; newPoints.push({ X: newX, Y: newY }); } doc.data = new InkField(newPoints); } } break; case 'hgt': const oWidth = NumCast(this.selectedDoc?._width); const oHeight = NumCast(this.selectedDoc?._height); const oX = NumCast(this.selectedDoc?.x); const oY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === 'up' ? 10 : -10)); InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight) * NumCast(this.selectedDoc?._width)); const docu = this.selectedDoc; if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) { const ink = Cast(docu.data, InkField)?.inkData; if (ink) { const newPoints: { X: number; Y: number }[] = []; for (var j = 0; j < ink.length; j++) { // (new x — oldx) + (oldxpoint * newWidt)/oldWidth const newX = NumCast(docu.x) - oX + (ink[j].X * NumCast(docu._width)) / oWidth; const newY = NumCast(docu.y) - oY + (ink[j].Y * NumCast(docu._height)) / oHeight; newPoints.push({ X: newX, Y: newY }); } docu.data = new InkField(newPoints); } } break; } }; getField(key: string) { //if (this.selectedDoc) { return Field.toString(this.selectedDoc?.[key] as Field); // } else { // return undefined as Opt; // } } @computed get shapeXps() { return this.getField('x'); } @computed get shapeYps() { return this.getField('y'); } @computed get shapeRot() { return this.getField('rotation'); } @computed get shapeHgt() { return this.getField('_height'); } @computed get shapeWid() { return this.getField('_width'); } set shapeXps(value) { this.selectedDoc && (this.selectedDoc.x = Number(value)); } set shapeYps(value) { this.selectedDoc && (this.selectedDoc.y = Number(value)); } set shapeRot(value) { this.selectedDoc && (this.selectedDoc.rotation = Number(value)); } set shapeWid(value) { const oldWidth = NumCast(this.selectedDoc?._width); this.selectedDoc && (this.selectedDoc._width = Number(value)); InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); } set shapeHgt(value) { const oldHeight = NumCast(this.selectedDoc?._height); this.selectedDoc && (this.selectedDoc._height = Number(value)); InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); } @computed get hgtInput() { return this.inputBoxDuo( 'hgt', this.shapeHgt, (val: string) => { if (!isNaN(Number(val))) { this.shapeHgt = val; } return true; }, 'H:', 'wid', this.shapeWid, (val: string) => { if (!isNaN(Number(val))) { this.shapeWid = val; } return true; }, 'W:' ); } @computed get XpsInput() { return this.inputBoxDuo( 'Xps', this.shapeXps, (val: string) => { if (val !== '0' && !isNaN(Number(val))) { this.shapeXps = val; } return true; }, 'X:', 'Yps', this.shapeYps, (val: string) => { if (val !== '0' && !isNaN(Number(val))) { this.shapeYps = val; } return true; }, 'Y:' ); } @computed get rotInput() { return this.inputBoxDuo( 'rot', this.shapeRot, (val: string) => { if (!isNaN(Number(val))) { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; } return true; }, '∠:', 'rot', this.shapeRot, (val: string) => { if (!isNaN(Number(val))) { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; } return true; }, '' ); } @observable private _fillBtn = false; @observable private _lineBtn = false; private _lastFill = '#D0021B'; private _lastLine = '#D0021B'; private _lastDash: any = '2'; @computed get colorFil() { const ccol = this.getField('fillColor') || ''; ccol && (this._lastFill = ccol); return ccol; } @computed get colorStk() { const ccol = this.getField('color') || ''; ccol && (this._lastLine = ccol); return ccol; } set colorFil(value) { value && (this._lastFill = value); this.selectedDoc && (this.selectedDoc.fillColor = value ? value : undefined); } set colorStk(value) { value && (this._lastLine = value); this.selectedDoc && (this.selectedDoc.color = value ? value : undefined); } colorButton(value: string, type: string, setter: () => {}) { // return
this.changeScrolling(false)} // onPointerLeave={e => this.changeScrolling(true)}> // return (
setter()))}>
{value === '' || value === 'transparent' ?

: ''}
); // //
; } @undoBatch @action switchStk = (color: ColorState) => { const val = String(color.hex); this.colorStk = val; return true; }; @undoBatch @action switchFil = (color: ColorState) => { const val = String(color.hex); this.colorFil = val; return true; }; colorPicker(setter: (color: string) => {}, type: string) { return ( ); } @computed get fillButton() { return this.colorButton(this.colorFil, 'fill', () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; return true; }); } @computed get lineButton() { return this.colorButton(this.colorStk, 'line', () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; return true; }); } @computed get fillPicker() { return this.colorPicker((color: string) => (this.colorFil = color), 'fil'); } @computed get linePicker() { return this.colorPicker((color: string) => (this.colorStk = color), 'stk'); } @computed get strokeAndFill() { return (
Fill:
{this.fillButton}
Stroke:
{this.lineButton}
{this._fillBtn ? this.fillPicker : ''} {this._lineBtn ? this.linePicker : ''}
); } @computed get solidStk() { return this.selectedDoc?.color && (!this.selectedDoc?.strokeDash || this.selectedDoc?.strokeDash === '0') ? true : false; } @computed get dashdStk() { return this.selectedDoc?.strokeDash || ''; } @computed get unStrokd() { return this.selectedDoc?.color ? true : false; } @computed get widthStk() { return this.getField('strokeWidth') || '1'; } @computed get markScal() { return Number(this.getField('strokeMakerScale') || '1'); } @computed get markHead() { return this.getField('strokeStartMarker') || ''; } @computed get markTail() { return this.getField('strokeEndMarker') || ''; } set solidStk(value) { this.dashdStk = ''; this.unStrokd = !value; } set dashdStk(value) { value && (this._lastDash = value) && (this.unStrokd = false); this.selectedDoc && (this.selectedDoc.strokeDash = value ? this._lastDash : undefined); } set markScal(value) { this.selectedDoc && (this.selectedDoc.strokeMarkerScale = Number(value)); } set widthStk(value) { this.selectedDoc && (this.selectedDoc.strokeWidth = Number(value)); } set unStrokd(value) { this.colorStk = value ? '' : this._lastLine; } set markHead(value) { this.selectedDoc && (this.selectedDoc.strokeStartMarker = value); } set markTail(value) { this.selectedDoc && (this.selectedDoc.strokeEndMarker = value); } @computed get stkInput() { return this.regInput('stk', this.widthStk, (val: string) => (this.widthStk = val)); } @computed get markScaleInput() { return this.regInput('scale', this.markScal.toString(), (val: string) => (this.markScal = Number(val))); } regInput = (key: string, value: any, setter: (val: string) => {}) => { return (
setter(e.target.value)} />
this.upDownButtons('up', key)))}>
this.upDownButtons('down', key)))}>
); }; @computed get widthAndDash() { return (
Width:
{this.stkInput}
(this.widthStk = e.target.value))} onMouseDown={e => { this._widthUndo = UndoManager.StartBatch('width undo'); }} onMouseUp={e => { this._widthUndo?.end(); this._widthUndo = undefined; }} />
Arrow Scale:
{/*
{this.markScalInput}
*/}
(this.markScal = +e.target.value))} onMouseDown={e => { this._widthUndo = UndoManager.StartBatch('scale undo'); }} onMouseUp={e => { this._widthUndo?.end(); this._widthUndo = undefined; }} />
Arrow Head:
(this.markHead = this.markHead ? '' : 'arrow')))} />
Arrow End:
(this.markTail = this.markTail ? '' : 'arrow')))} />
Dashed Line:
); } @undoBatch @action changeDash = () => { this.dashdStk = this.dashdStk === '2' ? '0' : '2'; }; @computed get appearanceEditor() { return (
{this.widthAndDash} {this.strokeAndFill}
); } @computed get transformEditor() { return (
{this.controlPointsButton} {this.hgtInput} {this.XpsInput} {this.rotInput}
); } @computed get optionsSubMenu() { return (
(this.inOptions = true))} onPointerLeave={action(() => (this.inOptions = false))}>
(this.openOptions = !this.openOptions))} style={{ backgroundColor: this.openOptions ? 'black' : '' }}> Options
{!this.openOptions ? null : (
)}
); } @computed get sharingSubMenu() { return (
(this.openSharing = !this.openSharing))} style={{ backgroundColor: this.openSharing ? 'black' : '' }}> Sharing {'&'} Permissions
{!this.openSharing ? null : (
{!Doc.noviceMode ? (
(this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} />
Layout
) : null} {/*
{"Re-distribute sharing settings"}
}>
*/}
{this.sharingTable}
)}
); } /** * Checks if a currentFilter (FilterDoc) exists on the current collection (if the Properties Panel + Filters submenu are open). * If it doesn't exist, it creates it. */ checkFilterDoc() { if (!this.selectedDoc?.currentFilter) this.selectedDoc!.currentFilter = FilterBox.createFilterDoc(); } /** * Creates a new currentFilter for this.filterDoc, */ createNewFilterDoc = () => { if (this.selectedDoc) { const curFilterDoc = DocCast(this.selectedDoc.currentFilter); const currentDocFilters = this.selectedDoc._docFilters; const currentDocRangeFilters = this.selectedDoc._docRangeFilters; this.selectedDoc._docFilters = new List(); this.selectedDoc._docRangeFilters = new List(); if (DocListCast(Doc.UserDoc().savedFilters).includes(curFilterDoc)) { curFilterDoc._docFiltersList = currentDocFilters; curFilterDoc._docRangeFiltersList = currentDocRangeFilters; this.selectedDoc.currentFilter = FilterBox.createFilterDoc(); } else { Doc.GetProto(curFilterDoc).data = undefined; Doc.GetProto(curFilterDoc).title = 'Unnamed Filter'; curFilterDoc._docFiltersList = undefined; curFilterDoc._docRangeFiltersList = undefined; } } }; /** * Updates this.filterDoc's currentFilter and saves the docFilters 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._docFiltersList; const currentDocFilters = this.selectedDoc._docFilters; this.selectedDoc._docFilters = new List(); (this.selectedDoc.currentFilter as Doc)._docFiltersList = currentDocFilters; this.selectedDoc.currentFilter = doc; doc._docFiltersList = new List(); this.selectedDoc._docFilters = savedDocFilters; const savedDocRangeFilters = doc._docRangeFiltersList; const currentDocRangeFilters = this.selectedDoc._docRangeFilters; this.selectedDoc._docRangeFilters = new List(); (this.selectedDoc.currentFilter as Doc)._docRangeFiltersList = currentDocRangeFilters; this.selectedDoc.currentFilter = doc; doc._docRangeFiltersList = new List(); this.selectedDoc._docRangeFilters = savedDocRangeFilters; } }; @computed get filtersSubMenu() { return !(this.selectedDoc?.currentFilter instanceof Doc) ? null : (
(this.openFilters = !this.openFilters))} style={{ backgroundColor: this.openFilters ? 'black' : '' }}> Filters
{!this.openFilters ? null : (
this.props.width} PanelHeight={this.selectedDoc.currentFilter[HeightSym]} renderDepth={0} scriptContext={this.selectedDoc.currentFilter} focus={emptyFunction} styleProvider={DefaultStyleProvider} isContentActive={returnTrue} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} createNewFilterDoc={this.createNewFilterDoc} updateFilterDoc={this.updateFilterDoc} docViewPath={returnEmptyDoclist} dontCenter="y" />
)}
); } @computed get inkSubMenu() { return ( <> {!this.isInk ? null : (
(this.openAppearance = !this.openAppearance))} style={{ backgroundColor: this.openAppearance ? 'black' : '' }}> Appearance
{!this.openAppearance ? null :
{this.appearanceEditor}
}
)} {this.isInk ? (
(this.openTransform = !this.openTransform))} style={{ backgroundColor: this.openTransform ? 'black' : '' }}> Transform
{this.openTransform ?
{this.transformEditor}
: null}
) : null} ); } @computed get fieldsSubMenu() { return (
(this.openFields = !this.openFields))} style={{ backgroundColor: this.openFields ? 'black' : '' }}> Fields {'&'} Tags
{!Doc.noviceMode && this.openFields ? (
{this.fieldsCheckbox}
Layout
) : null} {!this.openFields ? null :
{Doc.noviceMode ? this.noviceFields : this.expandedField}
}
); } @computed get contextsSubMenu() { return (
(this.openContexts = !this.openContexts))} style={{ backgroundColor: this.openContexts ? 'black' : '' }}> Other Contexts
{this.openContexts ?
{this.contexts}
: null}
); } @computed get linksSubMenu() { return (
(this.openLinks = !this.openLinks))} style={{ backgroundColor: this.openLinks ? 'black' : '' }}> Linked To
{this.openLinks ?
{this.links}
: null}
); } @computed get layoutSubMenu() { return (
(this.openLayout = !this.openLayout))} style={{ backgroundColor: this.openLayout ? 'black' : '' }}> Layout
{this.openLayout ?
{this.layoutPreview}
: null}
); } @computed get description() { return Field.toString(LinkManager.currentLink?.description as any as Field); } @computed get relationship() { return StrCast(LinkManager.currentLink?.linkRelationship); } @observable private relationshipButtonColor: string = ''; // @action // handleDescriptionChange = (e: React.ChangeEvent) => { this.description = e.target.value; } // handleRelationshipChange = (e: React.ChangeEvent) => { this.relationship = e.target.value; } @undoBatch handleDescriptionChange = action((value: string) => { if (LinkManager.currentLink && this.selectedDoc) { this.setDescripValue(value); return true; } }); @undoBatch handleLinkRelationshipChange = action((value: string) => { if (LinkManager.currentLink && this.selectedDoc) { this.setLinkRelationshipValue(value); return true; } }); @undoBatch setDescripValue = action((value: string) => { if (LinkManager.currentLink) { Doc.GetProto(LinkManager.currentLink).description = value; return true; } }); @undoBatch setLinkRelationshipValue = action((value: string) => { if (LinkManager.currentLink) { const prevRelationship = LinkManager.currentLink.linkRelationship as string; LinkManager.currentLink.linkRelationship = value; Doc.GetProto(LinkManager.currentLink).linkRelationship = value; const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes); const linkColorList = StrListCast(Doc.UserDoc().linkColorList); // 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 ); return true; } }); @undoBatch changeFollowBehavior = action((follow: string) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow)); @undoBatch changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior)); @undoBatch changeEffectDirection = action((effect: PresEffectDirection) => this.sourceAnchor && (this.sourceAnchor.linkAnimDirection = effect)); animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => { const lanch = this.sourceAnchor; const color = lanch?.linkAnimDirection === direction || (direction === PresEffectDirection.Center && !lanch?.linkAnimDirection) ? Colors.MEDIUM_BLUE : ''; return ( {direction}
}>
this.changeEffectDirection(direction)}> {icon ? : null}
); }; onSelectOutDesc = () => { this.setDescripValue(this.description); document.getElementById('link_description_input')?.blur(); }; onDescriptionKey = (e: React.KeyboardEvent) => { 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, undoBatch(action(() => LinkManager.currentLink && (LinkManager.currentLink[prop] = !LinkManager.currentLink[prop])))); }; @computed get destinationAnchor() { const ldoc = LinkManager.currentLink; const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch; return ldoc ? DocCast(ldoc.anchor2) : ldoc; } @computed get sourceAnchor() { const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; return selAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink); } toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc, value: any = true, ovalue: any = false, cb: (val: any) => any = val => val) => { anchor && setupMoveUpEvents( this, e, returnFalse, emptyFunction, undoBatch( action(() => { anchor[prop] = anchor[prop] === value ? ovalue : value; this.selectedDoc && cb(anchor[prop]); }) ) ); }; @computed get editRelationship() { return ( this.handleLinkRelationshipChange(e.currentTarget.value)} className="text" type="text" /> ); } @computed get editDescription() { return (