@streamscloud/kit 0.62.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/utils/color-helper.d.ts +4 -0
- package/dist/core/utils/color-helper.js +10 -0
- package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +17 -51
- package/dist/ui/cropper/image-editor-dialog/crop-and-resize.d.ts +4 -16
- package/dist/ui/cropper/image-editor-dialog/crop-and-resize.js +1 -14
- package/dist/ui/cropper/image-editor-dialog/index.d.ts +8 -7
- package/dist/ui/cropper/image-editor-dialog/index.js +8 -7
- package/dist/ui/cropper/image-editor-dialog/types.d.ts +1 -1
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte +149 -181
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte.d.ts +4 -13
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte +389 -147
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte.d.ts +13 -39
- package/dist/ui/cropper/img-cropper/frame-geometry.d.ts +82 -0
- package/dist/ui/cropper/img-cropper/frame-geometry.js +165 -0
- package/dist/ui/cropper/img-cropper/img-cropper-localization.d.ts +2 -3
- package/dist/ui/cropper/img-cropper/img-cropper-localization.js +12 -19
- package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.d.ts +14 -5
- package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.js +127 -33
- package/dist/ui/cropper/img-cropper/img-cropper-utils.d.ts +4 -51
- package/dist/ui/cropper/img-cropper/img-cropper-utils.js +35 -190
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +99 -34
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +405 -104
- package/dist/ui/cropper/img-cropper/index.d.ts +1 -2
- package/dist/ui/cropper/img-cropper/index.js +0 -1
- package/dist/ui/table/cmp.table-draggable.svelte +2 -2
- package/dist/ui/table/cmp.table-draggable.svelte.d.ts +5 -7
- package/dist/ui/table/cmp.table.svelte +2 -2
- package/dist/ui/table/cmp.table.svelte.d.ts +5 -7
- package/dist/ui/table/table-view.svelte.d.ts +2 -2
- package/dist/ui/table/table-view.svelte.js +11 -7
- package/package.json +1 -3
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte +0 -68
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte.d.ts +0 -21
- package/dist/ui/cropper/img-cropper/cropperjs-elements.d.ts +0 -35
- package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.d.ts +0 -40
- package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.js +0 -193
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.d.ts +0 -7
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.js +0 -51
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.d.ts +0 -7
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.js +0 -89
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.d.ts +0 -42
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.js +0 -1
|
@@ -8,8 +8,8 @@ type SelectionCallbacks<T> = {
|
|
|
8
8
|
};
|
|
9
9
|
/** Inline width declarations for a column's `th` / `td`: the preferred width plus, when set, the floor the browser must honour. */
|
|
10
10
|
export declare const columnWidthStyle: <T extends WithId>(column: ResolvedColumn<T>) => string;
|
|
11
|
-
/** Shared presentation wiring for the table views: selection-event subscriptions + the no-items placeholder decision (
|
|
12
|
-
export declare const createTableView: <T extends WithId>(model: () => TableModel<T>, on: () => SelectionCallbacks<T> | undefined,
|
|
11
|
+
/** Shared presentation wiring for the table views: selection-event subscriptions + the no-items placeholder decision (on/off gate + anti-flicker delay). */
|
|
12
|
+
export declare const createTableView: <T extends WithId>(model: () => TableModel<T>, on: () => SelectionCallbacks<T> | undefined, noItemsPlaceholderEnabled?: () => boolean | undefined, noItemsPlaceholderDelay?: () => number | undefined) => {
|
|
13
13
|
readonly showNoItemsPlaceholder: boolean;
|
|
14
14
|
};
|
|
15
15
|
export {};
|
|
@@ -4,9 +4,8 @@ export const columnWidthStyle = (column) => {
|
|
|
4
4
|
const width = column.cssWidth ? `width: ${column.cssWidth};` : 'width: auto;';
|
|
5
5
|
return column.minCssWidth ? `${width} min-width: ${column.minCssWidth};` : width;
|
|
6
6
|
};
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export const createTableView = (model, on, noItemsPlaceholder = () => undefined) => {
|
|
7
|
+
/** Shared presentation wiring for the table views: selection-event subscriptions + the no-items placeholder decision (on/off gate + anti-flicker delay). */
|
|
8
|
+
export const createTableView = (model, on, noItemsPlaceholderEnabled = () => undefined, noItemsPlaceholderDelay = () => undefined) => {
|
|
10
9
|
let showNoItemsPlaceholder = $state(false);
|
|
11
10
|
let timeout = 0;
|
|
12
11
|
$effect(() => untrack(() => {
|
|
@@ -19,20 +18,25 @@ export const createTableView = (model, on, noItemsPlaceholder = () => undefined)
|
|
|
19
18
|
return () => subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
20
19
|
}));
|
|
21
20
|
$effect(() => {
|
|
22
|
-
const decision = noItemsPlaceholder() ?? NO_ITEMS_PLACEHOLDER_DELAY_MS;
|
|
23
21
|
if (model().items.length) {
|
|
24
22
|
clearTimeout(timeout);
|
|
25
23
|
showNoItemsPlaceholder = false;
|
|
26
24
|
return;
|
|
27
25
|
}
|
|
28
|
-
if (
|
|
26
|
+
if (noItemsPlaceholderEnabled() === false) {
|
|
29
27
|
clearTimeout(timeout);
|
|
30
|
-
showNoItemsPlaceholder =
|
|
28
|
+
showNoItemsPlaceholder = false;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const delay = noItemsPlaceholderDelay() ?? 0;
|
|
32
|
+
if (delay <= 0) {
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
showNoItemsPlaceholder = true;
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
33
37
|
timeout = window.setTimeout(() => {
|
|
34
38
|
showNoItemsPlaceholder = !model().items.length;
|
|
35
|
-
},
|
|
39
|
+
}, delay);
|
|
36
40
|
return () => clearTimeout(timeout);
|
|
37
41
|
});
|
|
38
42
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamscloud/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.0",
|
|
4
4
|
"author": "StreamsCloud",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -489,7 +489,6 @@
|
|
|
489
489
|
"@urql/core": "^6.0.1",
|
|
490
490
|
"cleave-zen": "^0.0.17",
|
|
491
491
|
"colord": "^2.9.3",
|
|
492
|
-
"cropperjs": "^2.1.1",
|
|
493
492
|
"dequal": "^2.0.3",
|
|
494
493
|
"dompurify": "^3.4.5",
|
|
495
494
|
"fuse.js": "^7.3.0",
|
|
@@ -519,7 +518,6 @@
|
|
|
519
518
|
"autoprefixer": "^10.6.1",
|
|
520
519
|
"cleave-zen": "^0.0.17",
|
|
521
520
|
"colord": "^2.9.3",
|
|
522
|
-
"cropperjs": "^2.1.1",
|
|
523
521
|
"dequal": "^2.0.3",
|
|
524
522
|
"dompurify": "^3.4.5",
|
|
525
523
|
"eslint": "^10.10.0",
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
<script lang="ts">import { ImgCropperLocalization } from './img-cropper-localization';
|
|
2
|
-
const { cropper } = $props();
|
|
3
|
-
const loc = new ImgCropperLocalization();
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
{#if cropper.cropBoxVisible}
|
|
7
|
-
<div class="img-cropper-controls">
|
|
8
|
-
<button type="button" class="img-cropper-controls__button img-cropper-controls__button--apply" onclick={() => cropper.crop()}>{loc.apply}</button>
|
|
9
|
-
<button type="button" class="img-cropper-controls__button img-cropper-controls__button--cancel" onclick={() => cropper.clearSelection()}>
|
|
10
|
-
{loc.cancel}
|
|
11
|
-
</button>
|
|
12
|
-
</div>
|
|
13
|
-
{/if}
|
|
14
|
-
|
|
15
|
-
<!--
|
|
16
|
-
@component
|
|
17
|
-
Apply/Cancel controls for the image cropper. Renders conditionally when a crop box is active.
|
|
18
|
-
Can be placed anywhere in the layout.
|
|
19
|
-
|
|
20
|
-
### Props
|
|
21
|
-
| Prop | Type | Default | Description |
|
|
22
|
-
|---|---|---|---|
|
|
23
|
-
| `cropper` | `ImgCropper` | — | Cropper controller instance |
|
|
24
|
-
|
|
25
|
-
### CSS Custom Properties
|
|
26
|
-
| Property | Description | Default |
|
|
27
|
-
|---|---|---|
|
|
28
|
-
| `--sc-kit--img-cropper-controls--background` | Panel background | `transparent` |
|
|
29
|
-
-->
|
|
30
|
-
|
|
31
|
-
<style>.img-cropper-controls {
|
|
32
|
-
--_img-cropper-controls--background: var(--sc-kit--img-cropper-controls--background, transparent);
|
|
33
|
-
width: 15rem;
|
|
34
|
-
padding: var(--sc-kit--space--1);
|
|
35
|
-
display: flex;
|
|
36
|
-
gap: var(--sc-kit--space--3);
|
|
37
|
-
background: var(--_img-cropper-controls--background);
|
|
38
|
-
}
|
|
39
|
-
.img-cropper-controls__button {
|
|
40
|
-
font-size: var(--sc-kit--font-size--sm);
|
|
41
|
-
padding: var(--sc-kit--space--1) var(--sc-kit--space--4);
|
|
42
|
-
border-radius: var(--sc-kit--radius--sm);
|
|
43
|
-
max-width: 8rem;
|
|
44
|
-
height: var(--sc-kit--field--height--md);
|
|
45
|
-
display: inline-flex;
|
|
46
|
-
flex: 1;
|
|
47
|
-
justify-content: center;
|
|
48
|
-
align-items: center;
|
|
49
|
-
cursor: pointer;
|
|
50
|
-
transition: background-color var(--sc-kit--duration--base) linear, color var(--sc-kit--duration--base) linear;
|
|
51
|
-
}
|
|
52
|
-
.img-cropper-controls__button--apply {
|
|
53
|
-
color: var(--sc-kit--color--text--on-accent);
|
|
54
|
-
background: var(--sc-kit--color--success);
|
|
55
|
-
border: 0;
|
|
56
|
-
}
|
|
57
|
-
.img-cropper-controls__button--apply:hover {
|
|
58
|
-
background: var(--sc-kit--color--success--hover);
|
|
59
|
-
}
|
|
60
|
-
.img-cropper-controls__button--cancel {
|
|
61
|
-
color: var(--sc-kit--color--text--primary);
|
|
62
|
-
background: var(--sc-kit--color--bg--panel);
|
|
63
|
-
border: 1px solid var(--sc-kit--color--border);
|
|
64
|
-
box-shadow: var(--sc-kit--shadow--hairline);
|
|
65
|
-
}
|
|
66
|
-
.img-cropper-controls__button--cancel:hover {
|
|
67
|
-
background: var(--sc-kit--color--bg--hover);
|
|
68
|
-
}</style>
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { ImgCropper } from './img-cropper.svelte';
|
|
2
|
-
type Props = {
|
|
3
|
-
cropper: ImgCropper;
|
|
4
|
-
};
|
|
5
|
-
/**
|
|
6
|
-
* Apply/Cancel controls for the image cropper. Renders conditionally when a crop box is active.
|
|
7
|
-
* Can be placed anywhere in the layout.
|
|
8
|
-
*
|
|
9
|
-
* ### Props
|
|
10
|
-
* | Prop | Type | Default | Description |
|
|
11
|
-
* |---|---|---|---|
|
|
12
|
-
* | `cropper` | `ImgCropper` | — | Cropper controller instance |
|
|
13
|
-
*
|
|
14
|
-
* ### CSS Custom Properties
|
|
15
|
-
* | Property | Description | Default |
|
|
16
|
-
* |---|---|---|
|
|
17
|
-
* | `--sc-kit--img-cropper-controls--background` | Panel background | `transparent` |
|
|
18
|
-
*/
|
|
19
|
-
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
20
|
-
type Cmp = ReturnType<typeof Cmp>;
|
|
21
|
-
export default Cmp;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// Type declarations for CropperJS v2 web components.
|
|
2
|
-
// Extends svelteHTML.IntrinsicElements so the Svelte language server
|
|
3
|
-
// recognises <cropper-*> tags and their attributes in templates.
|
|
4
|
-
|
|
5
|
-
type CamelToKebab<S extends string> = S extends `${infer F}${infer R}`
|
|
6
|
-
? R extends Uncapitalize<R>
|
|
7
|
-
? `${Lowercase<F>}${CamelToKebab<R>}`
|
|
8
|
-
: `${Lowercase<F>}-${CamelToKebab<R>}`
|
|
9
|
-
: S;
|
|
10
|
-
|
|
11
|
-
type CropperElementProps<T extends HTMLElement> = {
|
|
12
|
-
[
|
|
13
|
-
K in keyof T as K extends keyof HTMLElement
|
|
14
|
-
? never
|
|
15
|
-
: K extends `$${string}`
|
|
16
|
-
? never
|
|
17
|
-
: T[K] extends (...args: never[]) => unknown
|
|
18
|
-
? never
|
|
19
|
-
: K extends string
|
|
20
|
-
? CamelToKebab<K>
|
|
21
|
-
: never
|
|
22
|
-
]?: (T[K] extends boolean ? boolean : T[K] extends number ? number | string : string) | null;
|
|
23
|
-
} & import('svelte/elements').HTMLAttributes<HTMLElement>;
|
|
24
|
-
|
|
25
|
-
declare namespace svelteHTML {
|
|
26
|
-
interface IntrinsicElements {
|
|
27
|
-
'cropper-canvas': CropperElementProps<import('cropperjs').CropperCanvas>;
|
|
28
|
-
'cropper-image': CropperElementProps<import('cropperjs').CropperImage>;
|
|
29
|
-
'cropper-shade': CropperElementProps<import('cropperjs').CropperShade>;
|
|
30
|
-
'cropper-handle': CropperElementProps<import('cropperjs').CropperHandle>;
|
|
31
|
-
'cropper-selection': CropperElementProps<import('cropperjs').CropperSelection>;
|
|
32
|
-
'cropper-grid': CropperElementProps<import('cropperjs').CropperGrid>;
|
|
33
|
-
'cropper-crosshair': CropperElementProps<import('cropperjs').CropperCrosshair>;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { CanvasGeometry, DragMode, ImgCropperResult, ImgCropperWorker, ImgCropperWorkerParams } from './img-cropper-worker.svelte';
|
|
2
|
-
import type { CropperCanvas, CropperImage, CropperSelection } from 'cropperjs';
|
|
3
|
-
export declare abstract class ImgCropperBaseWorker implements ImgCropperWorker {
|
|
4
|
-
cropBoxVisible: boolean;
|
|
5
|
-
dragMode: DragMode;
|
|
6
|
-
canvasGeometry: CanvasGeometry;
|
|
7
|
-
destroy: () => void;
|
|
8
|
-
protected _image: CropperImage;
|
|
9
|
-
protected _selection: CropperSelection;
|
|
10
|
-
protected _canvas: CropperCanvas;
|
|
11
|
-
protected _selectHandle: HTMLElement | null;
|
|
12
|
-
protected _originalSrc: string;
|
|
13
|
-
protected _sourceMime: string | null;
|
|
14
|
-
protected _visualWidth: number;
|
|
15
|
-
protected _visualHeight: number;
|
|
16
|
-
protected abstract readonly _centerMode: 'contain' | 'cover';
|
|
17
|
-
constructor(params: ImgCropperWorkerParams);
|
|
18
|
-
ready: () => Promise<void>;
|
|
19
|
-
rotate: () => Promise<void>;
|
|
20
|
-
zoomIn: () => void;
|
|
21
|
-
zoomOut: () => void;
|
|
22
|
-
reset: () => Promise<void>;
|
|
23
|
-
crop: (options?: {
|
|
24
|
-
fillColor?: string;
|
|
25
|
-
}) => Promise<ImgCropperResult | null>;
|
|
26
|
-
save: (options?: {
|
|
27
|
-
fillColor?: string;
|
|
28
|
-
}) => Promise<ImgCropperResult>;
|
|
29
|
-
clearSelection: () => void;
|
|
30
|
-
refit: () => Promise<void>;
|
|
31
|
-
enableCropMode: () => void;
|
|
32
|
-
protected _wrapTransformOp: (fn: () => void) => void;
|
|
33
|
-
protected _applyMoveMode: () => void;
|
|
34
|
-
protected _resolveOutputFormat: (fillColor: string | undefined) => {
|
|
35
|
-
mime: string;
|
|
36
|
-
quality: number;
|
|
37
|
-
};
|
|
38
|
-
protected _fitImage: () => Promise<void>;
|
|
39
|
-
protected abstract _computeCanvasSize(): void;
|
|
40
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { ColorHelper } from '../../../core/utils';
|
|
2
|
-
import { computeImageScale, computeNaturalOutputSize, handleSelectionBoundary } from './img-cropper-utils';
|
|
3
|
-
import { tick } from 'svelte';
|
|
4
|
-
const OPAQUE_MIME_TYPES = new Set(['image/jpeg', 'image/bmp']);
|
|
5
|
-
export class ImgCropperBaseWorker {
|
|
6
|
-
cropBoxVisible = $state(false);
|
|
7
|
-
dragMode = $state('move');
|
|
8
|
-
canvasGeometry = $state.raw({ aspectRatio: null, width: 0, height: 0 });
|
|
9
|
-
destroy;
|
|
10
|
-
_image;
|
|
11
|
-
_selection;
|
|
12
|
-
_canvas;
|
|
13
|
-
_selectHandle;
|
|
14
|
-
_originalSrc;
|
|
15
|
-
_sourceMime;
|
|
16
|
-
_visualWidth = 0;
|
|
17
|
-
_visualHeight = 0;
|
|
18
|
-
constructor(params) {
|
|
19
|
-
this._originalSrc = params.originalSrc;
|
|
20
|
-
this._sourceMime = params.sourceMime;
|
|
21
|
-
this._image = params.image;
|
|
22
|
-
this._selection = params.selection;
|
|
23
|
-
this._canvas = params.canvas;
|
|
24
|
-
this._selectHandle = params.selectHandle;
|
|
25
|
-
this.canvasGeometry = { aspectRatio: params.aspectRatio, width: 0, height: 0 };
|
|
26
|
-
const onSelectionChange = (e) => {
|
|
27
|
-
// CropperSelection emits before committing, so a change the boundary handler cancelled never reaches the selection.
|
|
28
|
-
const { width, height } = e.defaultPrevented ? this._selection : e.detail;
|
|
29
|
-
this.cropBoxVisible = width > 0 && height > 0;
|
|
30
|
-
};
|
|
31
|
-
let handlingBoundary = false;
|
|
32
|
-
const onBoundaryCheck = (e) => {
|
|
33
|
-
if (handlingBoundary) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
handlingBoundary = true;
|
|
37
|
-
try {
|
|
38
|
-
handleSelectionBoundary({ event: e, selection: this._selection, canvas: this._canvas });
|
|
39
|
-
}
|
|
40
|
-
finally {
|
|
41
|
-
handlingBoundary = false;
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
// Boundary check first — onSelectionChange reads defaultPrevented, which it must not set itself.
|
|
45
|
-
this._selection.addEventListener('change', onBoundaryCheck);
|
|
46
|
-
this._selection.addEventListener('change', onSelectionChange);
|
|
47
|
-
this.destroy = () => {
|
|
48
|
-
this._selection.removeEventListener('change', onSelectionChange);
|
|
49
|
-
this._selection.removeEventListener('change', onBoundaryCheck);
|
|
50
|
-
};
|
|
51
|
-
this._applyMoveMode();
|
|
52
|
-
}
|
|
53
|
-
ready = async () => {
|
|
54
|
-
this._image.src = this._originalSrc;
|
|
55
|
-
await this._fitImage();
|
|
56
|
-
this._selection.$clear();
|
|
57
|
-
};
|
|
58
|
-
rotate = async () => {
|
|
59
|
-
[this._visualWidth, this._visualHeight] = [this._visualHeight, this._visualWidth];
|
|
60
|
-
this._computeCanvasSize();
|
|
61
|
-
await tick();
|
|
62
|
-
this._wrapTransformOp(() => {
|
|
63
|
-
this._image.$rotate('90deg');
|
|
64
|
-
this._image.$center(this._centerMode);
|
|
65
|
-
});
|
|
66
|
-
};
|
|
67
|
-
zoomIn = () => {
|
|
68
|
-
this._image.$zoom(0.1);
|
|
69
|
-
};
|
|
70
|
-
zoomOut = () => {
|
|
71
|
-
this._image.$zoom(-0.1);
|
|
72
|
-
};
|
|
73
|
-
reset = async () => {
|
|
74
|
-
this._selection.$clear();
|
|
75
|
-
this._applyMoveMode();
|
|
76
|
-
this._image.src = this._originalSrc;
|
|
77
|
-
await this._fitImage();
|
|
78
|
-
};
|
|
79
|
-
crop = async (options) => {
|
|
80
|
-
// An empty selection exports a 0x0 canvas, and assigning its "data:," data URL leaves the cropper with an unloadable source.
|
|
81
|
-
if (this._selection.width <= 0 || this._selection.height <= 0) {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
const fillColor = options?.fillColor;
|
|
85
|
-
const imageScale = computeImageScale(this._image.$getTransform());
|
|
86
|
-
const outputSize = computeNaturalOutputSize({ displayWidth: this._selection.width, displayHeight: this._selection.height, imageScale });
|
|
87
|
-
const canvas = await this._selection.$toCanvas({
|
|
88
|
-
width: outputSize.width,
|
|
89
|
-
height: outputSize.height,
|
|
90
|
-
beforeDraw: (ctx, cvs) => {
|
|
91
|
-
if (fillColor && !ColorHelper.isTransparent(fillColor)) {
|
|
92
|
-
ctx.fillStyle = fillColor;
|
|
93
|
-
ctx.fillRect(0, 0, cvs.width, cvs.height);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
const format = this._resolveOutputFormat(fillColor);
|
|
98
|
-
const dataUrl = canvas.toDataURL(format.mime, format.quality);
|
|
99
|
-
this._applyMoveMode();
|
|
100
|
-
this._image.src = dataUrl;
|
|
101
|
-
await this._fitImage();
|
|
102
|
-
this._selection.$clear();
|
|
103
|
-
return { dataUrl, width: canvas.width, height: canvas.height, mimeType: format.mime };
|
|
104
|
-
};
|
|
105
|
-
save = async (options) => {
|
|
106
|
-
const fillColor = options?.fillColor;
|
|
107
|
-
const beforeDraw = (ctx, cvs) => {
|
|
108
|
-
if (fillColor && !ColorHelper.isTransparent(fillColor)) {
|
|
109
|
-
ctx.fillStyle = fillColor;
|
|
110
|
-
ctx.fillRect(0, 0, cvs.width, cvs.height);
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
// Compute output dimensions before micro-zoom so the result matches
|
|
114
|
-
// the source pixel density, not the display size.
|
|
115
|
-
const imageScale = computeImageScale(this._image.$getTransform());
|
|
116
|
-
// A zero-sized selection exports a 0x0 canvas, whose toDataURL is the unusable "data:," string.
|
|
117
|
-
const hasCropBox = this._selection.width > 0 && this._selection.height > 0;
|
|
118
|
-
const displayWidth = hasCropBox ? this._selection.width : this._canvas.offsetWidth;
|
|
119
|
-
const displayHeight = hasCropBox ? this._selection.height : this._canvas.offsetHeight;
|
|
120
|
-
const outputSize = computeNaturalOutputSize({ displayWidth, displayHeight, imageScale });
|
|
121
|
-
const toCanvasOptions = { width: outputSize.width, height: outputSize.height, beforeDraw };
|
|
122
|
-
// CropperJS $toCanvas exports only the visible portion of the canvas.
|
|
123
|
-
// Sub-pixel rounding can leave a 1px transparent edge when the image
|
|
124
|
-
// tightly fits the canvas. A micro-zoom nudges the image slightly past
|
|
125
|
-
// the boundary so the export captures the full area; the zoom is
|
|
126
|
-
// reversed immediately after.
|
|
127
|
-
const needsMicroZoom = !hasCropBox;
|
|
128
|
-
if (needsMicroZoom) {
|
|
129
|
-
// Forward zoom increases coverage — safe without suspending cover check
|
|
130
|
-
this._image.$zoom(0.003);
|
|
131
|
-
}
|
|
132
|
-
const canvas = hasCropBox ? await this._selection.$toCanvas(toCanvasOptions) : await this._canvas.$toCanvas(toCanvasOptions);
|
|
133
|
-
if (needsMicroZoom) {
|
|
134
|
-
// Reverse zoom shrinks image — must suspend cover check to avoid clamping
|
|
135
|
-
this._wrapTransformOp(() => this._image.$zoom(-0.003));
|
|
136
|
-
}
|
|
137
|
-
const format = this._resolveOutputFormat(fillColor);
|
|
138
|
-
const dataUrl = canvas.toDataURL(format.mime, format.quality);
|
|
139
|
-
return { dataUrl, width: canvas.width, height: canvas.height, mimeType: format.mime };
|
|
140
|
-
};
|
|
141
|
-
clearSelection = () => {
|
|
142
|
-
this._selection.$clear();
|
|
143
|
-
this._applyMoveMode();
|
|
144
|
-
};
|
|
145
|
-
refit = async () => {
|
|
146
|
-
this._selection.$clear();
|
|
147
|
-
this._applyMoveMode();
|
|
148
|
-
this._computeCanvasSize();
|
|
149
|
-
await tick();
|
|
150
|
-
this._wrapTransformOp(() => {
|
|
151
|
-
this._image.$resetTransform();
|
|
152
|
-
this._image.$center(this._centerMode);
|
|
153
|
-
});
|
|
154
|
-
};
|
|
155
|
-
enableCropMode = () => {
|
|
156
|
-
this._image.translatable = false;
|
|
157
|
-
if (this._selectHandle) {
|
|
158
|
-
this._selectHandle.setAttribute('action', 'select');
|
|
159
|
-
}
|
|
160
|
-
this.dragMode = 'crop';
|
|
161
|
-
};
|
|
162
|
-
_wrapTransformOp = (fn) => {
|
|
163
|
-
fn();
|
|
164
|
-
};
|
|
165
|
-
_applyMoveMode = () => {
|
|
166
|
-
this._image.translatable = true;
|
|
167
|
-
if (this._selectHandle) {
|
|
168
|
-
this._selectHandle.setAttribute('action', 'move');
|
|
169
|
-
}
|
|
170
|
-
this.dragMode = 'move';
|
|
171
|
-
this.cropBoxVisible = false;
|
|
172
|
-
};
|
|
173
|
-
_resolveOutputFormat = (fillColor) => {
|
|
174
|
-
if (this._sourceMime && OPAQUE_MIME_TYPES.has(this._sourceMime)) {
|
|
175
|
-
return { mime: 'image/jpeg', quality: 0.92 };
|
|
176
|
-
}
|
|
177
|
-
if (fillColor && !ColorHelper.isTransparent(fillColor)) {
|
|
178
|
-
return { mime: 'image/jpeg', quality: 0.92 };
|
|
179
|
-
}
|
|
180
|
-
return { mime: 'image/png', quality: 1 };
|
|
181
|
-
};
|
|
182
|
-
_fitImage = async () => {
|
|
183
|
-
const img = await this._image.$ready();
|
|
184
|
-
this._visualWidth = img.naturalWidth;
|
|
185
|
-
this._visualHeight = img.naturalHeight;
|
|
186
|
-
this._computeCanvasSize();
|
|
187
|
-
await tick();
|
|
188
|
-
this._wrapTransformOp(() => {
|
|
189
|
-
this._image.$resetTransform();
|
|
190
|
-
this._image.$center(this._centerMode);
|
|
191
|
-
});
|
|
192
|
-
};
|
|
193
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { ImgCropperBaseWorker } from './img-cropper-base-worker.svelte';
|
|
2
|
-
import type { ImgCropperWorkerParams } from './img-cropper-worker.svelte';
|
|
3
|
-
export declare class ImgCropperContainWorker extends ImgCropperBaseWorker {
|
|
4
|
-
protected readonly _centerMode: "contain";
|
|
5
|
-
constructor(params: ImgCropperWorkerParams);
|
|
6
|
-
protected _computeCanvasSize: () => void;
|
|
7
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { ImgCropperBaseWorker } from './img-cropper-base-worker.svelte';
|
|
2
|
-
import { fitInContainer, resolveCanvasGeometry } from './img-cropper-utils';
|
|
3
|
-
export class ImgCropperContainWorker extends ImgCropperBaseWorker {
|
|
4
|
-
_centerMode = 'contain';
|
|
5
|
-
constructor(params) {
|
|
6
|
-
super(params);
|
|
7
|
-
}
|
|
8
|
-
_computeCanvasSize = () => {
|
|
9
|
-
const container = this._canvas.parentElement;
|
|
10
|
-
if (!container) {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
const containerWidth = container.clientWidth;
|
|
14
|
-
const containerHeight = container.clientHeight;
|
|
15
|
-
const visualWidth = this._visualWidth;
|
|
16
|
-
const visualHeight = this._visualHeight;
|
|
17
|
-
const aspectRatio = this.canvasGeometry.aspectRatio;
|
|
18
|
-
if (containerWidth <= 0 || containerHeight <= 0 || visualWidth <= 0 || visualHeight <= 0) {
|
|
19
|
-
this.canvasGeometry = { aspectRatio, width: 0, height: 0 };
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
if (aspectRatio !== null) {
|
|
23
|
-
// Fixed ratio: fit in container, cap at natural image bounds
|
|
24
|
-
const fitted = fitInContainer({ ratio: aspectRatio, containerWidth, containerHeight });
|
|
25
|
-
const maxWidth = Math.max(visualWidth, visualHeight * aspectRatio);
|
|
26
|
-
const width = Math.min(fitted.width, maxWidth);
|
|
27
|
-
this.canvasGeometry = resolveCanvasGeometry({ aspectRatio, width, height: width / aspectRatio, maxWidth: fitted.width, maxHeight: containerHeight });
|
|
28
|
-
}
|
|
29
|
-
else if (visualWidth <= containerWidth && visualHeight <= containerHeight) {
|
|
30
|
-
const fitted = fitInContainer({ ratio: visualWidth / visualHeight, containerWidth, containerHeight });
|
|
31
|
-
this.canvasGeometry = resolveCanvasGeometry({
|
|
32
|
-
aspectRatio,
|
|
33
|
-
width: visualWidth,
|
|
34
|
-
height: visualHeight,
|
|
35
|
-
maxWidth: fitted.width,
|
|
36
|
-
maxHeight: containerHeight
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
// Free ratio, image too large — scale down to fit
|
|
41
|
-
const fitted = fitInContainer({ ratio: visualWidth / visualHeight, containerWidth, containerHeight });
|
|
42
|
-
this.canvasGeometry = resolveCanvasGeometry({
|
|
43
|
-
aspectRatio,
|
|
44
|
-
width: fitted.width,
|
|
45
|
-
height: fitted.height,
|
|
46
|
-
maxWidth: fitted.width,
|
|
47
|
-
maxHeight: containerHeight
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { ImgCropperBaseWorker } from './img-cropper-base-worker.svelte';
|
|
2
|
-
import type { ImgCropperWorkerParams } from './img-cropper-worker.svelte';
|
|
3
|
-
export declare class ImgCropperCoverWorker extends ImgCropperBaseWorker {
|
|
4
|
-
protected readonly _centerMode: "cover";
|
|
5
|
-
constructor(params: ImgCropperWorkerParams);
|
|
6
|
-
protected _computeCanvasSize: () => void;
|
|
7
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { ImgCropperBaseWorker } from './img-cropper-base-worker.svelte';
|
|
2
|
-
import { fitInContainer, handleCoverCheck, resolveCanvasGeometry } from './img-cropper-utils';
|
|
3
|
-
export class ImgCropperCoverWorker extends ImgCropperBaseWorker {
|
|
4
|
-
_centerMode = 'cover';
|
|
5
|
-
constructor(params) {
|
|
6
|
-
super(params);
|
|
7
|
-
// Dedup: handleCoverCheck calls $setTransform(clamped) which synchronously
|
|
8
|
-
// fires another transform event with a matrix that differs by ~0.0001px
|
|
9
|
-
// due to floating point. Without this guard CropperJS re-fires the same
|
|
10
|
-
// clamped transform endlessly, causing a stack overflow.
|
|
11
|
-
let prevMatrix = null;
|
|
12
|
-
const onCoverCheck = (e) => {
|
|
13
|
-
const { matrix } = e.detail;
|
|
14
|
-
if (prevMatrix && matrix.every((v, i) => Math.abs(v - prevMatrix[i]) < 0.01)) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
prevMatrix = matrix;
|
|
18
|
-
handleCoverCheck({ event: e, image: this._image, canvas: this._canvas });
|
|
19
|
-
prevMatrix = null;
|
|
20
|
-
};
|
|
21
|
-
this._image.addEventListener('transform', onCoverCheck);
|
|
22
|
-
this._wrapTransformOp = (fn) => {
|
|
23
|
-
this._image.removeEventListener('transform', onCoverCheck);
|
|
24
|
-
try {
|
|
25
|
-
fn();
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
this._image.addEventListener('transform', onCoverCheck);
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
const baseDestroy = this.destroy;
|
|
32
|
-
this.destroy = () => {
|
|
33
|
-
baseDestroy();
|
|
34
|
-
this._image.removeEventListener('transform', onCoverCheck);
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
_computeCanvasSize = () => {
|
|
38
|
-
const container = this._canvas.parentElement;
|
|
39
|
-
if (!container) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const containerWidth = container.clientWidth;
|
|
43
|
-
const containerHeight = container.clientHeight;
|
|
44
|
-
const visualWidth = this._visualWidth;
|
|
45
|
-
const visualHeight = this._visualHeight;
|
|
46
|
-
const aspectRatio = this.canvasGeometry.aspectRatio;
|
|
47
|
-
if (containerWidth <= 0 || containerHeight <= 0) {
|
|
48
|
-
this.canvasGeometry = { aspectRatio, width: 0, height: 0 };
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (aspectRatio !== null) {
|
|
52
|
-
const fitted = fitInContainer({ ratio: aspectRatio, containerWidth, containerHeight });
|
|
53
|
-
// Cap so the image can cover the canvas without upscaling
|
|
54
|
-
const maxWidth = visualWidth > 0 && visualHeight > 0 ? Math.min(visualWidth, visualHeight * aspectRatio) : fitted.width;
|
|
55
|
-
const width = Math.min(fitted.width, maxWidth);
|
|
56
|
-
this.canvasGeometry = resolveCanvasGeometry({ aspectRatio, width, height: width / aspectRatio, maxWidth: fitted.width, maxHeight: containerHeight });
|
|
57
|
-
}
|
|
58
|
-
else if (visualWidth <= 0 || visualHeight <= 0) {
|
|
59
|
-
this.canvasGeometry = resolveCanvasGeometry({
|
|
60
|
-
aspectRatio,
|
|
61
|
-
width: containerWidth,
|
|
62
|
-
height: containerHeight,
|
|
63
|
-
maxWidth: containerWidth,
|
|
64
|
-
maxHeight: containerHeight
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
else if (visualWidth <= containerWidth && visualHeight <= containerHeight) {
|
|
68
|
-
const fitted = fitInContainer({ ratio: visualWidth / visualHeight, containerWidth, containerHeight });
|
|
69
|
-
this.canvasGeometry = resolveCanvasGeometry({
|
|
70
|
-
aspectRatio,
|
|
71
|
-
width: visualWidth,
|
|
72
|
-
height: visualHeight,
|
|
73
|
-
maxWidth: fitted.width,
|
|
74
|
-
maxHeight: containerHeight
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
// Fit at image AR; per-axis capping collapses an oversized image to the container AR.
|
|
79
|
-
const fitted = fitInContainer({ ratio: visualWidth / visualHeight, containerWidth, containerHeight });
|
|
80
|
-
this.canvasGeometry = resolveCanvasGeometry({
|
|
81
|
-
aspectRatio,
|
|
82
|
-
width: fitted.width,
|
|
83
|
-
height: fitted.height,
|
|
84
|
-
maxWidth: fitted.width,
|
|
85
|
-
maxHeight: containerHeight
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { CropperCanvas, CropperImage, CropperSelection } from 'cropperjs';
|
|
2
|
-
export type DragMode = 'move' | 'crop';
|
|
3
|
-
export type CanvasGeometry = {
|
|
4
|
-
aspectRatio: number | null;
|
|
5
|
-
width: number;
|
|
6
|
-
height: number;
|
|
7
|
-
};
|
|
8
|
-
export type ImgCropperWorkerParams = {
|
|
9
|
-
originalSrc: string;
|
|
10
|
-
sourceMime: string | null;
|
|
11
|
-
image: CropperImage;
|
|
12
|
-
selection: CropperSelection;
|
|
13
|
-
canvas: CropperCanvas;
|
|
14
|
-
selectHandle: HTMLElement | null;
|
|
15
|
-
aspectRatio: number | null;
|
|
16
|
-
};
|
|
17
|
-
export type ImgCropperResult = {
|
|
18
|
-
dataUrl: string;
|
|
19
|
-
width: number;
|
|
20
|
-
height: number;
|
|
21
|
-
mimeType: string;
|
|
22
|
-
};
|
|
23
|
-
export interface ImgCropperWorker {
|
|
24
|
-
cropBoxVisible: boolean;
|
|
25
|
-
dragMode: DragMode;
|
|
26
|
-
canvasGeometry: CanvasGeometry;
|
|
27
|
-
destroy: () => void;
|
|
28
|
-
ready: () => Promise<void>;
|
|
29
|
-
rotate: () => void | Promise<void>;
|
|
30
|
-
zoomIn: () => void;
|
|
31
|
-
zoomOut: () => void;
|
|
32
|
-
reset: () => Promise<void>;
|
|
33
|
-
crop: (options?: {
|
|
34
|
-
fillColor?: string;
|
|
35
|
-
}) => Promise<ImgCropperResult | null>;
|
|
36
|
-
save: (options?: {
|
|
37
|
-
fillColor?: string;
|
|
38
|
-
}) => Promise<ImgCropperResult>;
|
|
39
|
-
clearSelection: () => void;
|
|
40
|
-
enableCropMode: () => void;
|
|
41
|
-
refit: () => void | Promise<void>;
|
|
42
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|