aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/ImageBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/ImageBox.tsx')
-rw-r--r--src/client/views/nodes/ImageBox.tsx153
1 files changed, 118 insertions, 35 deletions
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9590bcb15..f38ebba27 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -8,15 +8,16 @@ import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
import { createSchema } from '../../../fields/Schema';
import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, NumCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
-import { DocUtils } from '../../documents/Documents';
+import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
+import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../../views/ContextMenu';
@@ -26,10 +27,13 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComp
import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
+import { DocFocusOptions, OpenWhere } from './DocumentView';
import { FaceRectangles } from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import './ImageBox.scss';
+import { PinProps, PresBox } from './trails';
import React = require('react');
+import Color = require('color');
export const pageSchema = createSchema({
googlePhotosUrl: 'string',
@@ -48,44 +52,75 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(ImageBox, fieldKey);
}
+ private _ignoreScroll = false;
+ private _forcedScroll = false;
private _dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [name: string]: IReactionDisposer } = {};
- private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
+ private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined;
@observable _curSuffix = '';
@observable _uploadIcon = uploadIcons.idle;
+ constructor(props: any) {
+ super(props);
+ this.props.setContentView?.(this);
+ }
+
protected createDropTarget = (ele: HTMLDivElement) => {
this._dropDisposer?.();
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
};
- setViewSpec = (anchor: Doc, preview: boolean) => {}; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
- getAnchor = () => {
- const anchor = this._getAnchor?.(this._savedAnnotations);
- anchor && this.addDocument(anchor);
- return anchor ?? this.rootDoc;
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const anchor =
+ this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor
+ Docs.Create.ImageanchorDocument({
+ title: 'ImgAnchor:' + this.rootDoc.title,
+ presPanX: NumCast(this.layoutDoc._panX),
+ presPanY: NumCast(this.layoutDoc._panY),
+ presViewScale: Cast(this.layoutDoc._viewScale, 'number', null),
+ presTransition: 1000,
+ unrendered: true,
+ annotationOn: this.rootDoc,
+ });
+ if (anchor) {
+ if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
+ /* addAsAnnotation &&*/ this.addDocument(anchor);
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: true } }, this.rootDoc);
+ return anchor;
+ }
+ return this.rootDoc;
};
componentDidMount() {
- this.props.setContentView?.(this); // bcz: do not remove this. without it, stepping into an image in the lightbox causes an infinite loop....
this._disposers.sizer = reaction(
() => ({
forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes,
- scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth,
+ scrSize: (this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.rootDoc._viewScale, 1),
selected: this.props.isSelected(),
}),
- ({ forceFull, scrSize, selected }) => (this._curSuffix = this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 || !selected ? '_l' : '_o'),
+ ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'),
{ fireImmediately: true, delay: 1000 }
);
+ const layoutDoc = this.layoutDoc;
this._disposers.path = reaction(
() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }),
({ nativeSize, width }) => {
- if (true || !this.layoutDoc._height) {
+ if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) {
this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
}
},
{ fireImmediately: true }
);
+ this._disposers.scroll = reaction(
+ () => this.layoutDoc._scrollTop,
+ s_top => {
+ this._forcedScroll = true;
+ !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(s_top));
+ this._mainCont.current?.scrollTo({ top: NumCast(s_top) });
+ this._forcedScroll = false;
+ },
+ { fireImmediately: true }
+ );
}
componentWillUnmount() {
@@ -119,8 +154,25 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@undoBatch
resolution = () => (this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes);
+ @undoBatch
+ setUseAlt = () => (this.layoutDoc[this.fieldKey + '-useAlt'] = !this.layoutDoc[this.fieldKey + '-useAlt']);
@undoBatch
+ setNativeSize = action(() => {
+ const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.rootDoc._viewScale, 1);
+ const nscale = NumCast(this.props.PanelWidth()) / scaling;
+ const nh = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']);
+ const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']);
+ this.dataDoc[this.fieldKey + '-nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']) * nh;
+ this.dataDoc[this.fieldKey + '-nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']) * nh;
+ this.rootDoc._panX = nh * NumCast(this.rootDoc._panX);
+ this.rootDoc._panY = nh * NumCast(this.rootDoc._panY);
+ this.dataDoc._panXMax = this.dataDoc._panXMax ? nh * NumCast(this.dataDoc._panXMax) : undefined;
+ this.dataDoc._panXMin = this.dataDoc._panXMin ? nh * NumCast(this.dataDoc._panXMin) : undefined;
+ this.dataDoc._panYMax = this.dataDoc._panYMax ? nw * NumCast(this.dataDoc._panYMax) : undefined;
+ this.dataDoc._panYMin = this.dataDoc._panYMin ? nw * NumCast(this.dataDoc._panYMin) : undefined;
+ });
+ @undoBatch
rotate = action(() => {
const nw = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']);
const nh = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']);
@@ -138,7 +190,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const cropping = Doc.MakeCopy(region, true);
Doc.GetProto(region).lockedPosition = true;
Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
- Doc.GetProto(region).isPushpin = true;
+ Doc.GetProto(region).followLinkToggle = true;
this.addDocument(region);
const anchx = NumCast(cropping.x);
const anchy = NumCast(cropping.y);
@@ -154,6 +206,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const croppingProto = Doc.GetProto(cropping);
croppingProto.annotationOn = undefined;
croppingProto.isPrototype = true;
+ croppingProto.backgroundColor = undefined;
croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
croppingProto.type = DocumentType.IMG;
croppingProto.layout = ImageBox.LayoutString('data');
@@ -169,8 +222,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
croppingProto.panYMin = anchy / viewScale;
croppingProto.panYMax = anchh / viewScale;
if (addCrop) {
- DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' });
+ cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]();
+ cropping.y = NumCast(this.rootDoc.y);
+ this.props.addDocTab(cropping, OpenWhere.inParent);
}
+ DocumentManager.Instance.AddViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200));
this.props.bringToFront(cropping);
return cropping;
};
@@ -179,9 +236,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'expand-arrows-alt' });
- funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand-arrows-alt' });
- funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'expand-arrows-alt' });
+ funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
+ funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
+ funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
+ funcs.push({ description: `${this.layoutDoc[this.fieldKey + '-useAlt'] ? 'Show Alternate' : 'Show Primary'}`, event: this.setUseAlt, icon: 'image' });
+ funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'copy' });
if (!Doc.noviceMode) {
funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' });
@@ -230,7 +289,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const lower = url.href.toLowerCase();
if (url.protocol === 'data') return url.href;
if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href);
- if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return url.href; //Why is this here
+ if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`;
const ext = extname(url.href);
return url.href.replace(ext, this._curSuffix + ext);
@@ -246,6 +305,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return !tags ? null : <img id={'google-tags'} src={'/assets/google_tags.png'} />;
};
+ getScrollHeight = () => (this.props.fitWidth?.(this.rootDoc) !== false && NumCast(this.rootDoc._viewScale, 1) === NumCast(this.rootDoc._viewScaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
+
@computed
private get considerDownloadIcon() {
const data = this.dataDoc[this.fieldKey];
@@ -279,9 +340,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
setTimeout(
action(() => {
this._uploadIcon = idle;
- if (data) {
- dataDoc[this.fieldKey] = data;
- }
+ data && (dataDoc[this.fieldKey] = data);
}),
2000
);
@@ -309,11 +368,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
}
+ @observable _isHovering = false; // flag to switch between primary and alternate images on hover
@computed get content() {
TraceMobx();
- const srcpath = this.paths[0];
- const fadepath = this.paths[Math.min(1, this.paths.length - 1)];
+ const backAlpha = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor)).alpha();
+ const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
+ const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize;
const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']);
const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1;
@@ -330,25 +391,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
return (
- <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
- <div className="imageBox-fader" style={{ overflow: Array.from(this.props.docViewPath?.()).slice(-1)[0].fitWidth ? 'auto' : undefined }}>
+ <div className="imageBox-cont" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))} key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
+ <div className="imageBox-fader" style={{ opacity: backAlpha }}>
<img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
{fadepath === srcpath ? null : (
- <div className={`imageBox-fadeBlocker${this.props.isHovering?.() ? '-hover' : ''}`}>
- <img className="imageBox-fadeaway" key={'fadeaway'} src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
+ <div
+ className={`imageBox-fadeBlocker${(this._isHovering && this.layoutDoc[this.fieldKey + '-useAlt'] === undefined) || BoolCast(this.layoutDoc[this.fieldKey + '-useAlt']) ? '-hover' : ''}`}
+ style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}>
+ <img className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
</div>
)}
</div>
- {this.considerDownloadIcon}
+ {!Doc.noviceMode && this.considerDownloadIcon}
{this.considerGooglePhotosLink()}
<FaceRectangles document={this.dataDoc} color={'#0000FF'} backgroundColor={'#0000FF'} />
</div>
);
}
- screenToLocalTransform = this.props.ScreenToLocalTransform;
- contentFunc = () => [this.content];
-
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
@observable _marqueeing: number[] | undefined;
@@ -357,6 +417,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
TraceMobx();
return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />;
}
+ screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop) * this.props.ScreenToLocalTransform().Scale);
marqueeDown = (e: React.PointerEvent) => {
if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._viewScale, 1) <= NumCast(this.rootDoc.viewScaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
setupMoveUpEvents(
@@ -379,8 +440,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._marqueeing = undefined;
this.props.select(false);
};
- savedAnnotations = () => this._savedAnnotations;
+ focus = (anchor: Doc, options: DocFocusOptions) => this._ffref.current?.focus(anchor, options);
+ _ffref = React.createRef<CollectionFreeFormView>();
+ savedAnnotations = () => this._savedAnnotations;
render() {
TraceMobx();
const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
@@ -390,16 +453,31 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
className="imageBox"
onContextMenu={this.specificContextMenu}
ref={this._mainCont}
+ onScroll={action(e => {
+ if (!this._forcedScroll) {
+ if (this.layoutDoc._scrollTop || this._mainCont.current?.scrollTop) {
+ this._ignoreScroll = true;
+ this.layoutDoc._scrollTop = this._mainCont.current?.scrollTop;
+ this._ignoreScroll = false;
+ }
+ }
+ })}
style={{
width: this.props.PanelWidth() ? undefined : `100%`,
height: this.props.PanelWidth() ? undefined : `100%`,
pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
borderRadius,
+ overflow: this.layoutDoc.fitWidth || this.props.fitWidth?.(this.rootDoc) ? 'auto' : undefined,
}}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ ref={this._ffref}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
renderDepth={this.props.renderDepth + 1}
fieldKey={this.annotationKey}
+ styleProvider={this.props.styleProvider}
CollectionView={undefined}
isAnnotationOverlay={true}
annotationLayerHostsContent={true}
@@ -407,12 +485,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
PanelHeight={this.props.PanelHeight}
ScreenToLocalTransform={this.screenToLocalTransform}
select={emptyFunction}
+ focus={this.focus}
+ getScrollHeight={this.getScrollHeight}
NativeDimScaling={returnOne}
+ isAnyChildContentActive={returnFalse}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
addDocument={this.addDocument}>
- {this.contentFunc}
+ {this.content}
</CollectionFreeFormView>
{this.annotationLayer}
{!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
@@ -425,8 +506,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
addDocument={this.addDocument}
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotations}
+ selectionText={returnEmptyString}
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current}
+ highlightDragSrcColor={''}
anchorMenuCrop={this.crop}
/>
)}