MediaQuickview: supports grabbing
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				/ checkpr (pull_request) Successful in 1m32s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	/ checkpr (pull_request) Successful in 1m32s
				
			This commit is contained in:
		
							parent
							
								
									a15e2d62fe
								
							
						
					
					
						commit
						c1467ee32a
					
				
					 2 changed files with 135 additions and 31 deletions
				
			
		| 
						 | 
				
			
			@ -46,15 +46,18 @@
 | 
			
		|||
      height: 100%;
 | 
			
		||||
      max-width: 100vw;
 | 
			
		||||
      max-height: 100vh;
 | 
			
		||||
      contain: layout style size paint;
 | 
			
		||||
      contain: strict;
 | 
			
		||||
      display: grid;
 | 
			
		||||
      place-items: center;
 | 
			
		||||
 | 
			
		||||
      cursor: grab;
 | 
			
		||||
 | 
			
		||||
      >* {
 | 
			
		||||
        display: block;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
 | 
			
		||||
        object-fit: contain;
 | 
			
		||||
        object-position: center;
 | 
			
		||||
        transform-origin: 0 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -66,4 +69,10 @@
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.moving {
 | 
			
		||||
    >.Scaffold>.pages>.page {
 | 
			
		||||
      cursor: grabbing;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,7 @@
 | 
			
		|||
import { createCallback, createSubRoot } from "@solid-primitives/rootless";
 | 
			
		||||
import {
 | 
			
		||||
  batch,
 | 
			
		||||
  createMemo,
 | 
			
		||||
  createRenderEffect,
 | 
			
		||||
  createSignal,
 | 
			
		||||
  Index,
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +18,7 @@ import "./MediaQuickview.css";
 | 
			
		|||
import AppTopBar from "~material/AppTopBar";
 | 
			
		||||
import { IconButton } from "@suid/material";
 | 
			
		||||
import { Close } from "@suid/icons-material";
 | 
			
		||||
import { createStore, unwrap } from "solid-js/store";
 | 
			
		||||
 | 
			
		||||
function renderIsolateMediaQuickview(
 | 
			
		||||
  each: QuickviewMedia[],
 | 
			
		||||
| 
						 | 
				
			
			@ -53,35 +56,19 @@ export function createMediaQuickview() {
 | 
			
		|||
  return createCallback(renderIsolateMediaQuickview);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ImagePage(props: { src: string; alt?: string }) {
 | 
			
		||||
  const [scale, setScale] = createSignal(1);
 | 
			
		||||
  const [offsetX, setOffsetX] = createSignal(0);
 | 
			
		||||
  const [offsetY, setOffsetY] = createSignal(0);
 | 
			
		||||
 | 
			
		||||
  const onWheel = (event: WheelEvent & { currentTarget: HTMLElement }) => {
 | 
			
		||||
    // This is a de-facto standard for scaling:
 | 
			
		||||
    // Browsers will simulate ctrl + wheel for two point scaling gesture.
 | 
			
		||||
    if (event.ctrlKey) {
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
      event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
      const offset = event.deltaY;
 | 
			
		||||
 | 
			
		||||
      setScale((x) => x + offset / event.currentTarget.clientHeight);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
function ImagePage(props: {
 | 
			
		||||
  src: string;
 | 
			
		||||
  alt?: string;
 | 
			
		||||
  scale: number;
 | 
			
		||||
  offsetX: number;
 | 
			
		||||
  offsetY: number;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <img
 | 
			
		||||
      src={props.src}
 | 
			
		||||
      alt={props.alt}
 | 
			
		||||
      onWheel={onWheel}
 | 
			
		||||
      style={{
 | 
			
		||||
        transform: `scale(${scale()}) translateX(${offsetX()}) translateY(${offsetY()})`,
 | 
			
		||||
      }}
 | 
			
		||||
      onLoad={({ currentTarget }) => {
 | 
			
		||||
        const { top, left, width, height } =
 | 
			
		||||
          currentTarget.getBoundingClientRect();
 | 
			
		||||
        transform: `scale(${props.scale}) translateX(${-props.offsetX}px) translateY(${-props.offsetY}px)`,
 | 
			
		||||
      }}
 | 
			
		||||
    ></img>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			@ -101,8 +88,13 @@ export type MediaQuickviewProps = {
 | 
			
		|||
  onClose?(): void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function clamp<T extends number>(value: T, min: T, max: T) {
 | 
			
		||||
  return Math.max(Math.min(value, max), min);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const MediaQuickview: Component<MediaQuickviewProps> = (props) => {
 | 
			
		||||
  let root: HTMLDialogElement;
 | 
			
		||||
 | 
			
		||||
  const [lightOut, setLightOut] = createSignal(false);
 | 
			
		||||
 | 
			
		||||
  function onDialogClick(
 | 
			
		||||
| 
						 | 
				
			
			@ -123,6 +115,93 @@ const MediaQuickview: Component<MediaQuickviewProps> = (props) => {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [transformations, setTransformations] = createStore(
 | 
			
		||||
    [] as {
 | 
			
		||||
      scale: number;
 | 
			
		||||
      /**
 | 
			
		||||
       * positive = the left edge move towards left
 | 
			
		||||
       */
 | 
			
		||||
      offsetX: number;
 | 
			
		||||
      /**
 | 
			
		||||
       * positive = the top edge move towards top
 | 
			
		||||
       */
 | 
			
		||||
      offsetY: number;
 | 
			
		||||
    }[],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const transformationGetOrSetDefault = (index: number) => {
 | 
			
		||||
    if (transformations.length <= index) {
 | 
			
		||||
      setTransformations(index, {
 | 
			
		||||
        scale: 1,
 | 
			
		||||
        offsetX: 0,
 | 
			
		||||
        offsetY: 0,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return transformations[index];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onWheel = (
 | 
			
		||||
    index: number,
 | 
			
		||||
    event: WheelEvent & { currentTarget: HTMLElement },
 | 
			
		||||
  ) => {
 | 
			
		||||
    // This is a de-facto standard for scaling:
 | 
			
		||||
    // Browsers will simulate ctrl + wheel for two point scaling gesture on trackpad.
 | 
			
		||||
    if (event.ctrlKey) {
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
      event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
      const { clientX, clientY, currentTarget, deltaY } = event;
 | 
			
		||||
 | 
			
		||||
      const offset = -deltaY; // not reversed wheel: wheel up = scale +, wheel down = scale -
 | 
			
		||||
      const scaleOffset = offset / currentTarget.clientHeight;
 | 
			
		||||
 | 
			
		||||
      // Map the screen scale to the image viewport scale
 | 
			
		||||
      const userOriginX = clientX - currentTarget.clientLeft;
 | 
			
		||||
      const userOriginY = clientY - currentTarget.clientTop;
 | 
			
		||||
      setTransformations(index, ({ scale, offsetX, offsetY }) => {
 | 
			
		||||
        const nscale = scale + scaleOffset;
 | 
			
		||||
        return {
 | 
			
		||||
          offsetX: offsetX + (userOriginX * nscale - userOriginX),
 | 
			
		||||
          offsetY: offsetY + (userOriginY * nscale - userOriginX),
 | 
			
		||||
          scale: nscale,
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const [isMoving, setIsMoving] = createSignal(false);
 | 
			
		||||
  let movOriginX: number = 0,
 | 
			
		||||
    movOriginY: number = 0;
 | 
			
		||||
 | 
			
		||||
  const onMouseDown = (event: MouseEvent) => {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    setIsMoving(true);
 | 
			
		||||
    movOriginX = event.clientX;
 | 
			
		||||
    movOriginY = event.clientY;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onMouseMove = (index: number, event: MouseEvent) => {
 | 
			
		||||
    if (!isMoving()) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const dx = movOriginX - event.clientX;
 | 
			
		||||
    const dy = movOriginY - event.clientY ;
 | 
			
		||||
 | 
			
		||||
    setTransformations(index, ({ offsetX, offsetY }) => {
 | 
			
		||||
      return {
 | 
			
		||||
        offsetX: offsetX + dx,
 | 
			
		||||
        offsetY: offsetY + dy,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    movOriginX = event.clientX;
 | 
			
		||||
    movOriginY = event.clientY;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onMouseUp = (event: MouseEvent) => {
 | 
			
		||||
    setIsMoving(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <dialog
 | 
			
		||||
      ref={(e) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -132,7 +211,7 @@ const MediaQuickview: Component<MediaQuickviewProps> = (props) => {
 | 
			
		|||
        });
 | 
			
		||||
      }}
 | 
			
		||||
      class="MediaQuickview"
 | 
			
		||||
      classList={{ lightout: lightOut() }}
 | 
			
		||||
      classList={{ lightout: lightOut(), moving: isMoving() }}
 | 
			
		||||
      onClose={props.onClose}
 | 
			
		||||
      onCancel={props.onClose}
 | 
			
		||||
      onClick={onDialogClick}
 | 
			
		||||
| 
						 | 
				
			
			@ -140,7 +219,11 @@ const MediaQuickview: Component<MediaQuickviewProps> = (props) => {
 | 
			
		|||
      <Scaffold
 | 
			
		||||
        topbar={
 | 
			
		||||
          <AppTopBar>
 | 
			
		||||
            <IconButton color="inherit" onClick={(e) => root.close()}>
 | 
			
		||||
            <IconButton
 | 
			
		||||
              color="inherit"
 | 
			
		||||
              onClick={(e) => root.close()}
 | 
			
		||||
              disableFocusRipple
 | 
			
		||||
            >
 | 
			
		||||
              <Close />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </AppTopBar>
 | 
			
		||||
| 
						 | 
				
			
			@ -160,10 +243,22 @@ const MediaQuickview: Component<MediaQuickviewProps> = (props) => {
 | 
			
		|||
          <Index each={props.each}>
 | 
			
		||||
            {(item, index) => {
 | 
			
		||||
              return (
 | 
			
		||||
                <section class="page">
 | 
			
		||||
                <section
 | 
			
		||||
                  class="page"
 | 
			
		||||
                  onWheel={[onWheel, index]}
 | 
			
		||||
                  onMouseDown={onMouseDown}
 | 
			
		||||
                  onMouseMove={[onMouseMove, index]}
 | 
			
		||||
                  onMouseUp={onMouseUp}
 | 
			
		||||
                >
 | 
			
		||||
                  <Switch>
 | 
			
		||||
                    <Match when={item().cat === "image"}>
 | 
			
		||||
                      <ImagePage src={item().src} alt={item().alt} />
 | 
			
		||||
                      <ImagePage
 | 
			
		||||
                        src={item().src}
 | 
			
		||||
                        alt={item().alt}
 | 
			
		||||
                        scale={transformationGetOrSetDefault(index).scale}
 | 
			
		||||
                        offsetX={transformationGetOrSetDefault(index).offsetX}
 | 
			
		||||
                        offsetY={transformationGetOrSetDefault(index).offsetY}
 | 
			
		||||
                      />
 | 
			
		||||
                    </Match>
 | 
			
		||||
                  </Switch>
 | 
			
		||||
                </section>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue