2021-07-21 01:32:09 +02:00
|
|
|
<!--
|
|
|
|
Copyright: Ankitects Pty Ltd and contributors
|
|
|
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
-->
|
2021-07-28 20:17:31 +02:00
|
|
|
<script lang="ts">
|
2022-02-04 09:36:34 +01:00
|
|
|
import { onDestroy, tick } from "svelte";
|
2021-10-18 14:01:15 +02:00
|
|
|
|
2022-02-04 09:36:34 +01:00
|
|
|
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
|
|
|
|
import WithDropdown from "../../components/WithDropdown.svelte";
|
2021-10-18 14:01:15 +02:00
|
|
|
import HandleBackground from "../HandleBackground.svelte";
|
|
|
|
import HandleControl from "../HandleControl.svelte";
|
|
|
|
import HandleLabel from "../HandleLabel.svelte";
|
2022-02-04 09:36:34 +01:00
|
|
|
import HandleSelection from "../HandleSelection.svelte";
|
2022-02-03 05:52:11 +01:00
|
|
|
import { context } from "../rich-text-input";
|
2021-10-18 14:01:15 +02:00
|
|
|
import FloatButtons from "./FloatButtons.svelte";
|
|
|
|
import SizeSelect from "./SizeSelect.svelte";
|
2022-02-04 09:36:34 +01:00
|
|
|
import WithImageConstrained from "./WithImageConstrained.svelte";
|
2021-10-18 14:01:15 +02:00
|
|
|
|
2022-02-03 05:52:11 +01:00
|
|
|
const { container, styles } = context.get();
|
2021-10-18 14:01:15 +02:00
|
|
|
|
|
|
|
const sheetPromise = styles
|
|
|
|
.addStyleTag("imageOverlay")
|
2022-01-24 02:43:09 +01:00
|
|
|
.then((styleObject) => styleObject.element.sheet!);
|
2021-10-18 14:01:15 +02:00
|
|
|
|
|
|
|
let activeImage: HTMLImageElement | null = null;
|
|
|
|
|
|
|
|
async function resetHandle(): Promise<void> {
|
|
|
|
activeImage = null;
|
|
|
|
await tick();
|
|
|
|
}
|
|
|
|
|
|
|
|
async function maybeShowHandle(event: Event): Promise<void> {
|
|
|
|
await resetHandle();
|
|
|
|
|
|
|
|
if (event.target instanceof HTMLImageElement) {
|
|
|
|
const image = event.target;
|
2021-07-21 14:13:56 +02:00
|
|
|
|
2021-10-18 14:01:15 +02:00
|
|
|
if (!image.dataset.anki) {
|
|
|
|
activeImage = image;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-07-21 01:32:09 +02:00
|
|
|
|
2021-10-18 14:01:15 +02:00
|
|
|
container.addEventListener("click", maybeShowHandle);
|
|
|
|
container.addEventListener("blur", resetHandle);
|
|
|
|
container.addEventListener("key", resetHandle);
|
|
|
|
container.addEventListener("paste", resetHandle);
|
2021-07-21 02:15:10 +02:00
|
|
|
|
2021-07-28 20:17:31 +02:00
|
|
|
$: naturalWidth = activeImage?.naturalWidth;
|
|
|
|
$: naturalHeight = activeImage?.naturalHeight;
|
2021-07-26 22:31:47 +02:00
|
|
|
$: aspectRatio = naturalWidth && naturalHeight ? naturalWidth / naturalHeight : NaN;
|
2021-07-21 15:52:48 +02:00
|
|
|
|
2021-08-05 03:24:04 +02:00
|
|
|
let customDimensions: boolean = false;
|
2021-07-30 20:37:10 +02:00
|
|
|
let actualWidth = "";
|
|
|
|
let actualHeight = "";
|
2021-07-21 18:22:09 +02:00
|
|
|
|
2021-08-05 03:24:04 +02:00
|
|
|
function updateDimensions() {
|
2021-07-26 22:31:47 +02:00
|
|
|
/* we do not want the actual width, but rather the intended display width */
|
2021-07-29 20:37:51 +02:00
|
|
|
const widthAttribute = activeImage!.getAttribute("width");
|
2021-07-21 19:03:21 +02:00
|
|
|
customDimensions = false;
|
|
|
|
|
2021-07-29 20:37:51 +02:00
|
|
|
if (widthAttribute) {
|
|
|
|
actualWidth = widthAttribute;
|
2021-07-21 19:03:21 +02:00
|
|
|
customDimensions = true;
|
2021-07-21 18:32:07 +02:00
|
|
|
} else {
|
2021-07-21 19:03:21 +02:00
|
|
|
actualWidth = String(naturalWidth);
|
2021-07-21 18:32:07 +02:00
|
|
|
}
|
2021-07-21 18:22:09 +02:00
|
|
|
|
2021-07-29 20:37:51 +02:00
|
|
|
const heightAttribute = activeImage!.getAttribute("height");
|
|
|
|
if (heightAttribute) {
|
|
|
|
actualHeight = heightAttribute;
|
2021-07-21 19:03:21 +02:00
|
|
|
customDimensions = true;
|
2021-07-29 20:37:51 +02:00
|
|
|
} else if (customDimensions) {
|
|
|
|
actualHeight = String(Math.trunc(Number(actualWidth) / aspectRatio));
|
2021-07-21 18:32:07 +02:00
|
|
|
} else {
|
2021-07-21 19:03:21 +02:00
|
|
|
actualHeight = String(naturalHeight);
|
2021-07-21 18:32:07 +02:00
|
|
|
}
|
2021-07-30 02:07:41 +02:00
|
|
|
}
|
|
|
|
|
2021-09-15 17:05:43 +02:00
|
|
|
let updateSelection: () => Promise<void>;
|
2021-08-05 22:48:36 +02:00
|
|
|
|
2021-07-30 02:07:41 +02:00
|
|
|
async function updateSizesWithDimensions() {
|
2021-10-18 14:01:15 +02:00
|
|
|
await updateSelection?.();
|
2021-08-05 03:24:04 +02:00
|
|
|
updateDimensions();
|
2021-07-30 02:07:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/* window resizing */
|
|
|
|
const resizeObserver = new ResizeObserver(async () => {
|
2021-09-02 20:03:12 +02:00
|
|
|
await updateSizesWithDimensions();
|
2021-07-30 02:07:41 +02:00
|
|
|
});
|
|
|
|
|
2021-09-02 20:03:12 +02:00
|
|
|
$: observes = Boolean(activeImage);
|
|
|
|
$: if (observes) {
|
2021-09-02 21:01:29 +02:00
|
|
|
resizeObserver.observe(container);
|
2021-09-02 20:03:12 +02:00
|
|
|
} else {
|
2021-09-02 21:01:29 +02:00
|
|
|
resizeObserver.unobserve(container);
|
2021-09-02 20:03:12 +02:00
|
|
|
}
|
2021-07-30 02:07:41 +02:00
|
|
|
|
2021-07-28 20:17:31 +02:00
|
|
|
/* memoized position of image on resize start
|
|
|
|
* prevents frantic behavior when image shift into the next/previous line */
|
|
|
|
let getDragWidth: (event: PointerEvent) => number;
|
|
|
|
let getDragHeight: (event: PointerEvent) => number;
|
2021-07-21 01:32:09 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
function setPointerCapture({ detail }: CustomEvent): void {
|
2021-09-02 20:03:12 +02:00
|
|
|
const pointerId = detail.originalEvent.pointerId;
|
|
|
|
|
|
|
|
if (pointerId !== 1) {
|
2021-08-04 22:58:24 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const imageRect = activeImage!.getBoundingClientRect();
|
2021-07-28 20:17:31 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
const imageLeft = imageRect!.left;
|
|
|
|
const imageRight = imageRect!.right;
|
|
|
|
const [multX, imageX] = detail.west ? [-1, imageRight] : [1, -imageLeft];
|
2021-07-28 20:17:31 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
getDragWidth = ({ clientX }) => multX * clientX + imageX;
|
2021-07-28 23:41:46 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
const imageTop = imageRect!.top;
|
|
|
|
const imageBottom = imageRect!.bottom;
|
|
|
|
const [multY, imageY] = detail.north ? [-1, imageBottom] : [1, -imageTop];
|
2021-07-28 23:41:46 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
getDragHeight = ({ clientY }) => multY * clientY + imageY;
|
2021-07-28 23:41:46 +02:00
|
|
|
|
2021-08-04 22:58:24 +02:00
|
|
|
const target = detail.originalEvent.target as Element;
|
2021-09-02 20:03:12 +02:00
|
|
|
target.setPointerCapture(pointerId);
|
2021-08-04 22:58:24 +02:00
|
|
|
}
|
2021-07-21 01:32:09 +02:00
|
|
|
|
2021-07-30 01:03:21 +02:00
|
|
|
$: [minResizeWidth, minResizeHeight] =
|
|
|
|
aspectRatio > 1 ? [5 * aspectRatio, 5] : [5, 5 / aspectRatio];
|
|
|
|
|
2021-07-30 02:07:41 +02:00
|
|
|
async function resize(event: PointerEvent) {
|
2021-07-21 17:37:58 +02:00
|
|
|
const element = event.target! as Element;
|
|
|
|
|
|
|
|
if (!element.hasPointerCapture(event.pointerId)) {
|
2021-07-21 01:32:09 +02:00
|
|
|
return;
|
|
|
|
}
|
2021-07-21 02:15:10 +02:00
|
|
|
|
2021-07-28 20:17:31 +02:00
|
|
|
const dragWidth = getDragWidth(event);
|
|
|
|
const dragHeight = getDragHeight(event);
|
2021-07-21 02:15:10 +02:00
|
|
|
|
2021-07-26 22:31:47 +02:00
|
|
|
const widthIncrease = dragWidth / naturalWidth!;
|
|
|
|
const heightIncrease = dragHeight / naturalHeight!;
|
2021-07-21 02:37:07 +02:00
|
|
|
|
2021-08-05 22:48:36 +02:00
|
|
|
let width: number;
|
|
|
|
|
2021-07-26 23:09:14 +02:00
|
|
|
if (widthIncrease > heightIncrease) {
|
2021-07-30 01:03:21 +02:00
|
|
|
width = Math.max(Math.trunc(dragWidth), minResizeWidth);
|
2021-07-21 02:37:07 +02:00
|
|
|
} else {
|
2022-02-04 09:36:34 +01:00
|
|
|
const height = Math.max(Math.trunc(dragHeight), minResizeHeight);
|
2021-07-27 16:54:23 +02:00
|
|
|
width = Math.trunc(naturalWidth! * (height / naturalHeight!));
|
2021-07-21 02:37:07 +02:00
|
|
|
}
|
|
|
|
|
2022-01-08 02:47:12 +01:00
|
|
|
/**
|
|
|
|
* Image resizing add-ons previously used image.style.width to set the
|
|
|
|
* preferred width of an image. In these cases, if we'd only set
|
|
|
|
* image.width, there would be no visible effect on the image.
|
|
|
|
* To avoid confusion with users we'll clear image.style.width (for now).
|
|
|
|
*/
|
|
|
|
activeImage!.style.removeProperty("width");
|
|
|
|
if (activeImage!.getAttribute("style")?.length === 0) {
|
|
|
|
activeImage!.removeAttribute("style");
|
|
|
|
}
|
|
|
|
|
2021-07-28 20:17:31 +02:00
|
|
|
activeImage!.width = width;
|
2021-07-21 01:32:09 +02:00
|
|
|
}
|
|
|
|
|
2021-10-18 14:01:15 +02:00
|
|
|
onDestroy(() => {
|
|
|
|
resizeObserver.disconnect();
|
|
|
|
container.removeEventListener("click", maybeShowHandle);
|
|
|
|
container.removeEventListener("blur", resetHandle);
|
|
|
|
container.removeEventListener("key", resetHandle);
|
|
|
|
container.removeEventListener("paste", resetHandle);
|
|
|
|
});
|
2021-07-21 01:32:09 +02:00
|
|
|
</script>
|
|
|
|
|
2021-10-18 14:01:15 +02:00
|
|
|
<WithDropdown
|
|
|
|
drop="down"
|
|
|
|
autoOpen={true}
|
|
|
|
autoClose={false}
|
|
|
|
distance={3}
|
|
|
|
let:createDropdown
|
|
|
|
let:dropdownObject
|
|
|
|
>
|
|
|
|
{#await sheetPromise then sheet}
|
2021-08-06 03:53:44 +02:00
|
|
|
<WithImageConstrained
|
|
|
|
{sheet}
|
|
|
|
{container}
|
|
|
|
{activeImage}
|
2021-10-18 14:01:15 +02:00
|
|
|
maxWidth={250}
|
|
|
|
maxHeight={125}
|
2021-08-06 03:53:44 +02:00
|
|
|
on:update={() => {
|
|
|
|
updateSizesWithDimensions();
|
|
|
|
dropdownObject.update();
|
|
|
|
}}
|
|
|
|
let:toggleActualSize
|
|
|
|
let:active
|
|
|
|
>
|
|
|
|
{#if activeImage}
|
2021-08-06 02:19:36 +02:00
|
|
|
<HandleSelection
|
|
|
|
bind:updateSelection
|
|
|
|
{container}
|
|
|
|
image={activeImage}
|
|
|
|
on:mount={(event) => createDropdown(event.detail.selection)}
|
|
|
|
>
|
2021-09-02 21:01:29 +02:00
|
|
|
<HandleBackground on:dblclick={toggleActualSize} />
|
2021-08-06 02:19:36 +02:00
|
|
|
|
2021-10-18 14:01:15 +02:00
|
|
|
<HandleLabel on:mount={updateDimensions}>
|
2021-08-06 02:19:36 +02:00
|
|
|
<span>{actualWidth}×{actualHeight}</span>
|
|
|
|
{#if customDimensions}
|
|
|
|
<span>(Original: {naturalWidth}×{naturalHeight})</span
|
|
|
|
>
|
|
|
|
{/if}
|
|
|
|
</HandleLabel>
|
|
|
|
|
|
|
|
<HandleControl
|
|
|
|
{active}
|
2021-08-31 22:52:23 +02:00
|
|
|
activeSize={8}
|
2021-08-06 02:19:36 +02:00
|
|
|
offsetX={5}
|
|
|
|
offsetY={5}
|
|
|
|
on:pointerclick={(event) => {
|
|
|
|
if (active) {
|
|
|
|
setPointerCapture(event);
|
|
|
|
}
|
|
|
|
}}
|
2021-08-06 03:53:44 +02:00
|
|
|
on:pointermove={(event) => {
|
|
|
|
resize(event);
|
|
|
|
updateSizesWithDimensions();
|
|
|
|
dropdownObject.update();
|
|
|
|
}}
|
2021-08-06 02:19:36 +02:00
|
|
|
/>
|
|
|
|
</HandleSelection>
|
2021-10-18 14:01:15 +02:00
|
|
|
<ButtonDropdown on:click={updateSizesWithDimensions}>
|
2022-02-03 05:52:11 +01:00
|
|
|
<FloatButtons
|
|
|
|
image={activeImage}
|
|
|
|
on:update={dropdownObject.update}
|
|
|
|
/>
|
|
|
|
<SizeSelect {active} on:click={toggleActualSize} />
|
2021-08-06 02:19:36 +02:00
|
|
|
</ButtonDropdown>
|
2021-08-06 03:53:44 +02:00
|
|
|
{/if}
|
|
|
|
</WithImageConstrained>
|
2021-10-18 14:01:15 +02:00
|
|
|
{/await}
|
|
|
|
</WithDropdown>
|