@dosgato/dialog 0.0.30 → 0.0.32
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/FieldChooserLink.svelte +52 -14
- package/FieldChooserLink.svelte.d.ts +3 -0
- package/chooser/ChooserAPI.d.ts +23 -0
- package/chooser/Details.svelte +4 -2
- package/chooser/Thumbnail.svelte +1 -1
- package/imagecropper/FieldImageCropper.svelte +377 -0
- package/imagecropper/FieldImageCropper.svelte.d.ts +25 -0
- package/imagecropper/ImageCropperStore.d.ts +15 -0
- package/imagecropper/ImageCropperStore.js +96 -0
- package/imagecropper/imagecropper.d.ts +20 -0
- package/imagecropper/imagecropper.js +1 -0
- package/imagecropper/index.d.ts +1 -0
- package/imagecropper/index.js +1 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +5 -1
package/FieldChooserLink.svelte
CHANGED
|
@@ -20,6 +20,9 @@ export let urlEntry = false;
|
|
|
20
20
|
export let initialSource = undefined;
|
|
21
21
|
export let initialPath = undefined;
|
|
22
22
|
export let helptext = undefined;
|
|
23
|
+
export let selectedAsset = undefined;
|
|
24
|
+
// TODO: add a mime type acceptance prop, maybe a regex or function, to prevent users from
|
|
25
|
+
// choosing unacceptable mime types
|
|
23
26
|
const formStore = getContext(FORM_CONTEXT);
|
|
24
27
|
const inheritedPath = getContext(FORM_INHERITED_PATH);
|
|
25
28
|
const finalPath = [inheritedPath, path].filter(isNotBlank).join('.');
|
|
@@ -45,25 +48,60 @@ function onChange(setVal) {
|
|
|
45
48
|
}
|
|
46
49
|
async function userUrlEntry() {
|
|
47
50
|
const url = this.value;
|
|
51
|
+
store.clearPreview();
|
|
52
|
+
let found = false;
|
|
48
53
|
if (chooserClient.findByUrl) {
|
|
49
54
|
const item = await chooserClient.findByUrl(url);
|
|
50
|
-
if (item)
|
|
51
|
-
|
|
55
|
+
if (item) {
|
|
56
|
+
found = true;
|
|
57
|
+
if ((item.type === 'page' && !pages) || // they typed the URL for a page but we don't allow pages right now
|
|
58
|
+
(item.type === 'folder' && !folders) || // they typed the URL for an asset folder but not allowed
|
|
59
|
+
(item.type === 'asset' && !assets) || // they typed the URL for an asset but not allowed
|
|
60
|
+
(item.type === 'asset' && !item.image && images) // they typed the URL for a non-image asset but we only want images
|
|
61
|
+
) {
|
|
62
|
+
selectedAsset = {
|
|
63
|
+
type: 'raw',
|
|
64
|
+
id: undefined,
|
|
65
|
+
url
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
selectedAsset = { ...item, url };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
52
72
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
if (!found) {
|
|
74
|
+
try {
|
|
75
|
+
const _ = new URL(url);
|
|
76
|
+
const newVal = chooserClient.urlToValue?.(url) ?? url;
|
|
77
|
+
selectedAsset = {
|
|
78
|
+
type: 'raw',
|
|
79
|
+
id: newVal,
|
|
80
|
+
url
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
selectedAsset = {
|
|
85
|
+
type: 'raw',
|
|
86
|
+
id: undefined,
|
|
87
|
+
url
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
formStore.setField(finalPath, selectedAsset?.id);
|
|
61
92
|
formStore.dirtyField(finalPath);
|
|
62
93
|
}
|
|
63
|
-
let selectedAsset;
|
|
64
94
|
async function updateSelected(..._) {
|
|
65
|
-
if ($value && selectedAsset?.id !== $value)
|
|
95
|
+
if ($value && selectedAsset?.id !== $value) {
|
|
66
96
|
selectedAsset = await chooserClient.findById($value);
|
|
97
|
+
try {
|
|
98
|
+
if (!selectedAsset)
|
|
99
|
+
selectedAsset = { type: 'raw', id: $value, url: chooserClient.valueToUrl?.($value) ?? $value };
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
console.error(e);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
67
105
|
}
|
|
68
106
|
$: updateSelected($value);
|
|
69
107
|
</script>
|
|
@@ -76,8 +114,8 @@ $: updateSelected($value);
|
|
|
76
114
|
</div>
|
|
77
115
|
{/if}
|
|
78
116
|
<div class="dialog-chooser-entry">
|
|
79
|
-
{#if urlEntry
|
|
80
|
-
<input type="text" value={selectedAsset?.url ?? ''} on:change={userUrlEntry}>
|
|
117
|
+
{#if urlEntry}
|
|
118
|
+
<input type="text" value={selectedAsset?.url ?? ''} on:change={userUrlEntry} on:keyup={userUrlEntry}>
|
|
81
119
|
{/if}
|
|
82
120
|
<button type="button" on:click={show} aria-describedby={getDescribedBy([descid, messagesid, helptextid])}>Select {#if value}New{/if} {#if assets && pages}Link Target{:else if images}Image{:else if assets}Asset{:else}Page{/if}</button>
|
|
83
121
|
</div>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
import { type RawURL } from './chooser';
|
|
3
|
+
import type { AnyItem } from './chooser/ChooserAPI';
|
|
2
4
|
declare const __propDef: {
|
|
3
5
|
props: {
|
|
4
6
|
id?: string | undefined;
|
|
@@ -15,6 +17,7 @@ declare const __propDef: {
|
|
|
15
17
|
initialSource?: string | undefined;
|
|
16
18
|
initialPath?: string | undefined;
|
|
17
19
|
helptext?: string | undefined;
|
|
20
|
+
selectedAsset?: AnyItem | RawURL;
|
|
18
21
|
};
|
|
19
22
|
events: {
|
|
20
23
|
[evt: string]: CustomEvent<any>;
|
package/chooser/ChooserAPI.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ export interface Client<F = any> {
|
|
|
8
8
|
findById: (id: string) => Promise<AnyItem | undefined>;
|
|
9
9
|
findByUrl?: (url: string) => Promise<AnyItem | undefined>;
|
|
10
10
|
urlToValue?: (url: string) => string;
|
|
11
|
+
/**
|
|
12
|
+
* If the form is preloaded with a raw URL, findById returns undefined, and the client implements
|
|
13
|
+
* urlToValue, we will need a way to decode/reverse what urlToValue does. This function should
|
|
14
|
+
* do that.
|
|
15
|
+
*/
|
|
16
|
+
valueToUrl?: (value: string) => string;
|
|
11
17
|
upload: (source: string, path: string, files: FileList) => Promise<void>;
|
|
12
18
|
}
|
|
13
19
|
export interface Source {
|
|
@@ -30,11 +36,28 @@ interface Item {
|
|
|
30
36
|
path: string;
|
|
31
37
|
name: string;
|
|
32
38
|
source: string;
|
|
39
|
+
/**
|
|
40
|
+
* Identifier for the urlEntry input
|
|
41
|
+
*
|
|
42
|
+
* When urlEntry prop is true, the user gets a text field where they can freely
|
|
43
|
+
* enter a string to identify the object being chosen. This value will be
|
|
44
|
+
* placed in that field when they use the chooser.
|
|
45
|
+
*
|
|
46
|
+
* It should be reversible with the `findByUrl` function provided by your chooser
|
|
47
|
+
* client, but note that the `findByUrl` function can accept multiple URLs that
|
|
48
|
+
* all point to the same resource. If the user types anything that can identify
|
|
49
|
+
* a resource, the resource will show up in the "Details" area, but the URL the user
|
|
50
|
+
* typed will NOT change to the one provided by this property. We try not
|
|
51
|
+
* to rewrite values in the form fields where possible, because it can disrupt the
|
|
52
|
+
* user's interaction.
|
|
53
|
+
*/
|
|
54
|
+
url: string;
|
|
33
55
|
}
|
|
34
56
|
export interface Folder extends Item {
|
|
35
57
|
type: 'folder';
|
|
36
58
|
hasChildren: boolean;
|
|
37
59
|
acceptsUpload: boolean;
|
|
60
|
+
url: string;
|
|
38
61
|
}
|
|
39
62
|
export interface Page extends Item {
|
|
40
63
|
type: 'page';
|
package/chooser/Details.svelte
CHANGED
|
@@ -4,11 +4,13 @@ export let item;
|
|
|
4
4
|
|
|
5
5
|
<ul class="dialog-chooser-info" aria-live="polite">
|
|
6
6
|
{#if item.type !== 'raw'}
|
|
7
|
-
<li>{item.
|
|
7
|
+
<li>{item.path}</li>
|
|
8
|
+
{:else if item.id}
|
|
9
|
+
<li>External Link<br>{item.url}</li>
|
|
8
10
|
{/if}
|
|
9
11
|
{#if item.type === 'asset' && item.image}
|
|
10
12
|
<li>{item.image.width}x{item.image.height}</li>
|
|
11
|
-
{:else if item.type === 'page'}
|
|
13
|
+
{:else if item.type === 'page' && item.title}
|
|
12
14
|
<li>{item.title}</li>
|
|
13
15
|
{:else if item.type === 'folder'}
|
|
14
16
|
<li>{item.path}</li>
|
package/chooser/Thumbnail.svelte
CHANGED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
<script>import { isNotBlank, isNotNull } from 'txstate-utils';
|
|
2
|
+
import FieldStandard from '../FieldStandard.svelte';
|
|
3
|
+
import { ImageCropperStore } from './ImageCropperStore';
|
|
4
|
+
export let id = undefined;
|
|
5
|
+
export let path;
|
|
6
|
+
export let imageSrc;
|
|
7
|
+
export let selectionAspectRatio = 1;
|
|
8
|
+
export let minSelection = 0; // percentage of image, a value 0-1
|
|
9
|
+
export let snapDistance = 0;
|
|
10
|
+
export let label = '';
|
|
11
|
+
export let required = false;
|
|
12
|
+
export let conditional = undefined;
|
|
13
|
+
export let helptext = undefined;
|
|
14
|
+
let minSelectionWidth = 0;
|
|
15
|
+
let minSelectionHeight = 0;
|
|
16
|
+
let localCoord = null;
|
|
17
|
+
const oldGlobalCoord = { x: 0, y: 0 };
|
|
18
|
+
let initialGlobalCoord = null;
|
|
19
|
+
let globalOffsetX = 0;
|
|
20
|
+
let globalOffsetY = 0;
|
|
21
|
+
let initialGlobalDragCoord = null;
|
|
22
|
+
let initialDragLeft = 0;
|
|
23
|
+
let initialDragTop = 0;
|
|
24
|
+
let altKey = false;
|
|
25
|
+
let ctrlKey = false;
|
|
26
|
+
const pseudoEvent = { clientX: 0, clientY: 0 };
|
|
27
|
+
let shieldDiv;
|
|
28
|
+
let image;
|
|
29
|
+
function widthPercent(val) {
|
|
30
|
+
return val / imageWidth * 100;
|
|
31
|
+
}
|
|
32
|
+
function heightPercent(val) {
|
|
33
|
+
return val / imageHeight * 100;
|
|
34
|
+
}
|
|
35
|
+
const defaultSelection = {
|
|
36
|
+
selection: {
|
|
37
|
+
left: undefined,
|
|
38
|
+
top: undefined,
|
|
39
|
+
width: undefined,
|
|
40
|
+
height: undefined,
|
|
41
|
+
visible: false,
|
|
42
|
+
shieldVisible: false,
|
|
43
|
+
track: false,
|
|
44
|
+
drag: false
|
|
45
|
+
},
|
|
46
|
+
crop: {
|
|
47
|
+
imagecropbottom: 0,
|
|
48
|
+
imagecropleft: 0,
|
|
49
|
+
imagecropright: 0,
|
|
50
|
+
imagecroptop: 0
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
let imageWidth;
|
|
54
|
+
let imageHeight;
|
|
55
|
+
$: imageAspectRatio = imageWidth / imageHeight;
|
|
56
|
+
$: maxContainerWidth = (image ? Math.min(image.width, 700) : '700');
|
|
57
|
+
const store = new ImageCropperStore(defaultSelection);
|
|
58
|
+
const { selection, crop } = store;
|
|
59
|
+
function startSelection(e) {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
e.stopPropagation();
|
|
62
|
+
if (e.altKey)
|
|
63
|
+
altKey = e.altKey;
|
|
64
|
+
if (e.ctrlKey)
|
|
65
|
+
ctrlKey = e.ctrlKey;
|
|
66
|
+
if (!$selection.visible) {
|
|
67
|
+
localCoord = { x: e.offsetX + 1, y: e.offsetY + 1 };
|
|
68
|
+
initialGlobalCoord = { x: e.clientX, y: e.clientY };
|
|
69
|
+
oldGlobalCoord.x = e.clientX;
|
|
70
|
+
oldGlobalCoord.y = e.clientY;
|
|
71
|
+
let selectionWidth = 0;
|
|
72
|
+
let selectionHeight = 0;
|
|
73
|
+
if (minSelection > 0) {
|
|
74
|
+
if (imageAspectRatio > selectionAspectRatio) {
|
|
75
|
+
// use height for min selection calculation
|
|
76
|
+
selectionHeight = imageHeight * minSelection;
|
|
77
|
+
selectionWidth = (16.0 * selectionHeight) / 9.0; // TODO: Why is this assuming a 16:9 ratio? That's how it's working Gato, but probably isn't right.
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// use width for min selection calculation
|
|
81
|
+
selectionWidth = imageWidth * minSelection;
|
|
82
|
+
selectionHeight = (9.0 * selectionWidth) / 16.0;
|
|
83
|
+
}
|
|
84
|
+
minSelectionWidth = selectionWidth;
|
|
85
|
+
minSelectionHeight = selectionHeight;
|
|
86
|
+
}
|
|
87
|
+
store.draw(e.offsetX, e.offsetY, selectionWidth, selectionHeight, imageWidth, imageHeight);
|
|
88
|
+
store.setTrack(true);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function adjustInitialSelection(e, setVal) {
|
|
92
|
+
// Getting position and size.
|
|
93
|
+
if (altKey) {
|
|
94
|
+
// Pressing alt allows the user to move the selection while creating it.
|
|
95
|
+
globalOffsetX += e.clientX - oldGlobalCoord.x;
|
|
96
|
+
globalOffsetY += e.clientY - oldGlobalCoord.y;
|
|
97
|
+
}
|
|
98
|
+
// If the user moved the mouse from right to left instead of left to right
|
|
99
|
+
const invertedHor = e.clientX - (initialGlobalCoord.x + globalOffsetX) < 0;
|
|
100
|
+
// If the user moved the mouse from bottom to top instead of top to bottom
|
|
101
|
+
const invertedVer = e.clientY - (initialGlobalCoord.y + globalOffsetY) < 0;
|
|
102
|
+
let width = Math.abs(e.clientX - (initialGlobalCoord.x + globalOffsetX));
|
|
103
|
+
let height = Math.abs(e.clientY - (initialGlobalCoord.y + globalOffsetY));
|
|
104
|
+
let left = localCoord.x + globalOffsetX;
|
|
105
|
+
let top = localCoord.y + globalOffsetY;
|
|
106
|
+
if (invertedHor)
|
|
107
|
+
left -= width;
|
|
108
|
+
if (invertedVer)
|
|
109
|
+
top -= height;
|
|
110
|
+
const leftEdge = left;
|
|
111
|
+
if (leftEdge < 0 || (leftEdge <= snapDistance && !ctrlKey)) {
|
|
112
|
+
width += leftEdge;
|
|
113
|
+
left = 0;
|
|
114
|
+
}
|
|
115
|
+
const topEdge = top;
|
|
116
|
+
if (topEdge < 0 || (topEdge <= snapDistance && !ctrlKey)) {
|
|
117
|
+
height += topEdge;
|
|
118
|
+
top = 0;
|
|
119
|
+
}
|
|
120
|
+
const rightEdge = imageWidth - width - left;
|
|
121
|
+
if (rightEdge < 0 || (rightEdge <= snapDistance && !ctrlKey))
|
|
122
|
+
width += rightEdge;
|
|
123
|
+
const bottomEdge = imageHeight - height - top;
|
|
124
|
+
if (bottomEdge < 0 || (bottomEdge <= snapDistance && !ctrlKey))
|
|
125
|
+
height += bottomEdge;
|
|
126
|
+
if (width > selectionAspectRatio * height) {
|
|
127
|
+
if (invertedHor)
|
|
128
|
+
left += width - selectionAspectRatio * height;
|
|
129
|
+
width = selectionAspectRatio * height;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
if (invertedVer)
|
|
133
|
+
top += height - width / selectionAspectRatio;
|
|
134
|
+
height = width / selectionAspectRatio;
|
|
135
|
+
}
|
|
136
|
+
if (width < minSelectionWidth || height < minSelectionHeight) {
|
|
137
|
+
width = minSelectionWidth;
|
|
138
|
+
height = minSelectionHeight;
|
|
139
|
+
}
|
|
140
|
+
store.draw(left, top, width, height, imageWidth, imageHeight);
|
|
141
|
+
setVal($crop);
|
|
142
|
+
}
|
|
143
|
+
function onMouseUp(setVal) {
|
|
144
|
+
return (e) => {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
e.stopPropagation();
|
|
147
|
+
if (e.altKey)
|
|
148
|
+
altKey = e.altKey;
|
|
149
|
+
if (e.ctrlKey)
|
|
150
|
+
ctrlKey = e.ctrlKey;
|
|
151
|
+
if ($selection.track) {
|
|
152
|
+
if (initialGlobalCoord.x === e.clientX && initialGlobalCoord.y === e.clientY) {
|
|
153
|
+
reset(setVal);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
adjustInitialSelection(e, setVal);
|
|
157
|
+
store.setShieldVisibility(true);
|
|
158
|
+
oldGlobalCoord.x = e.clientX;
|
|
159
|
+
oldGlobalCoord.y = e.clientY;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (e.target === shieldDiv || e.target === image) {
|
|
163
|
+
reset(setVal);
|
|
164
|
+
}
|
|
165
|
+
store.setTrack(false);
|
|
166
|
+
store.setDrag(false);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function onMouseMove(setVal) {
|
|
170
|
+
return (e) => {
|
|
171
|
+
if (e.altKey)
|
|
172
|
+
altKey = e.altKey;
|
|
173
|
+
if (e.ctrlKey)
|
|
174
|
+
ctrlKey = e.ctrlKey;
|
|
175
|
+
if ($selection.track || $selection.drag) {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
pseudoEvent.clientX = e.clientX;
|
|
178
|
+
pseudoEvent.clientY = e.clientY;
|
|
179
|
+
if ($selection.track)
|
|
180
|
+
adjustInitialSelection(pseudoEvent, setVal);
|
|
181
|
+
else if ($selection.drag)
|
|
182
|
+
adjustSelectionDrag(pseudoEvent, setVal);
|
|
183
|
+
oldGlobalCoord.x = e.clientX;
|
|
184
|
+
oldGlobalCoord.y = e.clientY;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function startDragSelection(e) {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
e.stopPropagation();
|
|
191
|
+
if (e.altKey)
|
|
192
|
+
altKey = e.altKey;
|
|
193
|
+
if (e.ctrlKey)
|
|
194
|
+
ctrlKey = e.ctrlKey;
|
|
195
|
+
initialGlobalDragCoord = { x: e.clientX, y: e.clientY };
|
|
196
|
+
initialDragLeft = $selection.left;
|
|
197
|
+
initialDragTop = $selection.top;
|
|
198
|
+
store.setDrag(true);
|
|
199
|
+
}
|
|
200
|
+
function onKey(e, setVal) {
|
|
201
|
+
altKey = e.altKey;
|
|
202
|
+
ctrlKey = e.ctrlKey;
|
|
203
|
+
if ($selection.track || $selection.drag) {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
pseudoEvent.preventDefault = function () { };
|
|
206
|
+
pseudoEvent.clientX = oldGlobalCoord.x;
|
|
207
|
+
pseudoEvent.clientY = oldGlobalCoord.y;
|
|
208
|
+
pseudoEvent.altKey = e.altKey;
|
|
209
|
+
pseudoEvent.ctrlKey = e.ctrlKey;
|
|
210
|
+
}
|
|
211
|
+
if ($selection.track)
|
|
212
|
+
adjustInitialSelection(pseudoEvent, setVal);
|
|
213
|
+
else if ($selection.drag)
|
|
214
|
+
onMouseMove.call(pseudoEvent, setVal);
|
|
215
|
+
}
|
|
216
|
+
function onKeyDown(setVal) {
|
|
217
|
+
return (e) => {
|
|
218
|
+
if ((!altKey && e.altKey) || (!ctrlKey && e.ctrlKey))
|
|
219
|
+
onKey(e, setVal);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function onKeyUp(setVal) {
|
|
223
|
+
return (e) => {
|
|
224
|
+
if ((!altKey && e.altKey) || (!ctrlKey && e.ctrlKey))
|
|
225
|
+
onKey(e, setVal);
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function adjustSelectionDrag(e, setVal) {
|
|
229
|
+
let left = initialDragLeft + e.clientX - initialGlobalDragCoord.x;
|
|
230
|
+
let top = initialDragTop + e.clientY - initialGlobalDragCoord.y;
|
|
231
|
+
// Adjusting position to account for edge snapping
|
|
232
|
+
const leftEdge = left;
|
|
233
|
+
if (leftEdge < 0 || (leftEdge <= snapDistance && !ctrlKey))
|
|
234
|
+
left = 0;
|
|
235
|
+
const topEdge = top;
|
|
236
|
+
if (topEdge < 0 || (topEdge <= snapDistance && !ctrlKey))
|
|
237
|
+
top = 0;
|
|
238
|
+
const rightEdge = imageWidth - $selection.width - left;
|
|
239
|
+
if (rightEdge < 0 || (rightEdge <= snapDistance && !ctrlKey))
|
|
240
|
+
left += rightEdge;
|
|
241
|
+
const bottomEdge = imageHeight - $selection.height - top;
|
|
242
|
+
if (bottomEdge < 0 || (bottomEdge <= snapDistance && !ctrlKey))
|
|
243
|
+
top += bottomEdge;
|
|
244
|
+
store.moveSelectionTo(left, top, imageWidth, imageHeight);
|
|
245
|
+
setVal($crop);
|
|
246
|
+
}
|
|
247
|
+
function maximize(setVal) {
|
|
248
|
+
store.maximize(imageWidth, imageHeight, selectionAspectRatio);
|
|
249
|
+
setVal($crop);
|
|
250
|
+
}
|
|
251
|
+
function reset(setVal) {
|
|
252
|
+
localCoord = null;
|
|
253
|
+
initialGlobalCoord = null;
|
|
254
|
+
store.reset();
|
|
255
|
+
setVal($crop);
|
|
256
|
+
altKey = false;
|
|
257
|
+
ctrlKey = false;
|
|
258
|
+
}
|
|
259
|
+
let storeInitialized = false;
|
|
260
|
+
function init(value) {
|
|
261
|
+
if (!storeInitialized && imageWidth > 0) {
|
|
262
|
+
if (value) {
|
|
263
|
+
store.setCrop(value.imagecropleft, value.imagecroptop, value.imagecropright, value.imagecropbottom, imageWidth, imageHeight);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
store.maximize(imageWidth, imageHeight, selectionAspectRatio);
|
|
267
|
+
}
|
|
268
|
+
storeInitialized = true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
</script>
|
|
272
|
+
|
|
273
|
+
<FieldStandard bind:id {label} {path} {required} {conditional} {helptext} let:value let:setVal>
|
|
274
|
+
{#if isNotBlank(imageSrc)}
|
|
275
|
+
{@const _ = init(value)}
|
|
276
|
+
<div class="cropper-wrapper">
|
|
277
|
+
<div class="crop-image-container" on:mousedown={startSelection} on:mouseup={onMouseUp(setVal)} on:mousemove={onMouseMove(setVal)} on:keydown={onKeyDown(setVal)} on:keyup={onKeyUp(setVal)} bind:clientWidth={imageWidth} bind:clientHeight={imageHeight} style="max-width: {maxContainerWidth}px">
|
|
278
|
+
<img bind:this={image} class="crop-image" src={imageSrc} alt=""/>
|
|
279
|
+
<div class='selectionLine divSelectionHor divSelectionTop' class:visible={$selection.visible} style={isNotNull($selection.left) ? `left: ${$selection.left}px; top: ${$selection.top}px; width: ${$selection.width}px` : ''}></div>
|
|
280
|
+
<div class='selectionLine divSelectionVer divSelectionRight' class:visible={$selection.visible} style={isNotNull($selection.left) ? `left: ${$selection.left + $selection.width - 1}px; top: ${$selection.top}px; height: ${$selection.height}px` : ''}></div>
|
|
281
|
+
<div class='selectionLine divSelectionHor divSelectionBottom' class:visible={$selection.visible} style={isNotNull($selection.left) ? `left: ${$selection.left}px; top: ${$selection.top + $selection.height - 1}px; width: ${$selection.width}px` : ''}></div>
|
|
282
|
+
<div class='selectionLine divSelectionVer divSelectionLeft' class:visible={$selection.visible} style={isNotNull($selection.left) ? `left: ${$selection.left}px; top: ${$selection.top}px; height: ${$selection.height}px` : ''}></div>
|
|
283
|
+
<img src="{imageSrc}" class='imageCropSelection' alt="" class:visible={$selection.visible} style={isNotNull($selection.left) ? `clip-path: polygon(${widthPercent($selection.left)}% ${heightPercent($selection.top)}%, ${widthPercent($selection.left + $selection.width)}% ${heightPercent($selection.top)}%, ${widthPercent($selection.left + $selection.width)}% ${heightPercent($selection.top + $selection.height)}%, ${widthPercent($selection.left)}% ${heightPercent($selection.top + $selection.height)}%)` : ''} on:mousedown={startDragSelection}/>
|
|
284
|
+
<div bind:this={shieldDiv} class="divShield" class:visible={$selection.shieldVisible} ></div>
|
|
285
|
+
</div>
|
|
286
|
+
<div class="action-buttons">
|
|
287
|
+
<button class='btn-center-max' on:click={() => maximize(setVal)}>Center and Maximize</button>
|
|
288
|
+
<button class='btn-clear' on:click={() => reset(setVal)}>Clear</button>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="cropper-instructions">
|
|
291
|
+
Click and drag to select a section of your image to use.
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
{:else}
|
|
295
|
+
Image not selected
|
|
296
|
+
{/if}
|
|
297
|
+
</FieldStandard>
|
|
298
|
+
|
|
299
|
+
<style>
|
|
300
|
+
.crop-image-container {
|
|
301
|
+
position: relative;
|
|
302
|
+
}
|
|
303
|
+
.crop-image-container .crop-image {
|
|
304
|
+
max-width: 100%;
|
|
305
|
+
max-height: 100%;
|
|
306
|
+
z-index: 3;
|
|
307
|
+
cursor: crosshair;
|
|
308
|
+
display: block;
|
|
309
|
+
position: relative;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.selectionLine {
|
|
313
|
+
visibility: hidden;
|
|
314
|
+
}
|
|
315
|
+
.selectionLine.visible {
|
|
316
|
+
visibility: visible;
|
|
317
|
+
}
|
|
318
|
+
.imageCropSelection {
|
|
319
|
+
visibility: hidden;
|
|
320
|
+
}
|
|
321
|
+
.imageCropSelection.visible {
|
|
322
|
+
visibility: visible;
|
|
323
|
+
}
|
|
324
|
+
.imageCropSelection {
|
|
325
|
+
position: absolute;
|
|
326
|
+
top: 0;
|
|
327
|
+
left: 0;
|
|
328
|
+
display: block;
|
|
329
|
+
z-index: 9;
|
|
330
|
+
visibility: hidden;
|
|
331
|
+
overflow: hidden;
|
|
332
|
+
max-width:100%;
|
|
333
|
+
max-height: 100%;
|
|
334
|
+
}
|
|
335
|
+
.divShield {
|
|
336
|
+
position: absolute;
|
|
337
|
+
left: 0;
|
|
338
|
+
top: 0;
|
|
339
|
+
background: #000;
|
|
340
|
+
z-index: 4;
|
|
341
|
+
cursor: default;
|
|
342
|
+
visibility: hidden;
|
|
343
|
+
filter: opacity(80%);
|
|
344
|
+
opacity: 0.8;
|
|
345
|
+
width: 100%;
|
|
346
|
+
height: 100%;
|
|
347
|
+
}
|
|
348
|
+
.divShield.visible {
|
|
349
|
+
visibility: visible;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.divSelectionHor {
|
|
353
|
+
position: absolute;
|
|
354
|
+
left: 0;
|
|
355
|
+
top: 0;
|
|
356
|
+
width: 1px;
|
|
357
|
+
height: 1px;
|
|
358
|
+
font: 1px/1px verdana, sans-serif;
|
|
359
|
+
background: repeating-linear-gradient(to right, #000, #000 5px, white 5px, transparent 10px);
|
|
360
|
+
z-index: 10;
|
|
361
|
+
cursor: crosshair;
|
|
362
|
+
visibility: hidden;
|
|
363
|
+
}
|
|
364
|
+
.divSelectionVer {
|
|
365
|
+
position: absolute;
|
|
366
|
+
left: 0;
|
|
367
|
+
top: 0;
|
|
368
|
+
width: 1px;
|
|
369
|
+
height: 1px;
|
|
370
|
+
font: 1px/1px verdana, sans-serif;
|
|
371
|
+
background: repeating-linear-gradient(to bottom, #000, #000 5px, white 5px, transparent 10px);
|
|
372
|
+
z-index: 10;
|
|
373
|
+
cursor: crosshair;
|
|
374
|
+
visibility: hidden;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: {
|
|
4
|
+
id?: string | undefined;
|
|
5
|
+
path: string;
|
|
6
|
+
imageSrc: string;
|
|
7
|
+
selectionAspectRatio?: number;
|
|
8
|
+
minSelection?: number;
|
|
9
|
+
snapDistance?: number;
|
|
10
|
+
label?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
conditional?: boolean | undefined;
|
|
13
|
+
helptext?: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
events: {
|
|
16
|
+
[evt: string]: CustomEvent<any>;
|
|
17
|
+
};
|
|
18
|
+
slots: {};
|
|
19
|
+
};
|
|
20
|
+
export type FieldImageCropperProps = typeof __propDef.props;
|
|
21
|
+
export type FieldImageCropperEvents = typeof __propDef.events;
|
|
22
|
+
export type FieldImageCropperSlots = typeof __propDef.slots;
|
|
23
|
+
export default class FieldImageCropper extends SvelteComponentTyped<FieldImageCropperProps, FieldImageCropperEvents, FieldImageCropperSlots> {
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Store } from '@txstate-mws/svelte-store';
|
|
2
|
+
import type { ICropperStore } from './imagecropper';
|
|
3
|
+
export declare class ImageCropperStore extends Store<ICropperStore> {
|
|
4
|
+
selection: import("@txstate-mws/svelte-store").DerivedStore<import("./imagecropper").ICropSelection, ICropperStore>;
|
|
5
|
+
crop: import("@txstate-mws/svelte-store").DerivedStore<import("./imagecropper").ImageCropperOutput, ICropperStore>;
|
|
6
|
+
draw(left: number, top: number, width: number, height: number, imageWidth: number, imageHeight: number): void;
|
|
7
|
+
setCrop(cropLeft: number, cropTop: number, cropRight: number, cropBottom: number, imageWidth: number, imageHeight: number): void;
|
|
8
|
+
setTrack(track: boolean): void;
|
|
9
|
+
setShieldVisibility(vis: boolean): void;
|
|
10
|
+
setDrag(drag: boolean): void;
|
|
11
|
+
setSelectionVisibility(visible: boolean): void;
|
|
12
|
+
moveSelectionTo(left: number, top: number, imageWidth: number, imageHeight: number): void;
|
|
13
|
+
maximize(imageWidth: number, imageHeight: number, selectionAspectRatio: number): void;
|
|
14
|
+
reset(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Store, derivedStore } from '@txstate-mws/svelte-store';
|
|
2
|
+
export class ImageCropperStore extends Store {
|
|
3
|
+
selection = derivedStore(this, 'selection');
|
|
4
|
+
crop = derivedStore(this, 'crop');
|
|
5
|
+
draw(left, top, width, height, imageWidth, imageHeight) {
|
|
6
|
+
// calculate new values for crop
|
|
7
|
+
const cropLeft = left / imageWidth;
|
|
8
|
+
const cropTop = top / imageHeight;
|
|
9
|
+
const cropRight = (left + width) / imageWidth;
|
|
10
|
+
const cropBottom = (top + height) / imageHeight;
|
|
11
|
+
this.update(v => ({
|
|
12
|
+
...v,
|
|
13
|
+
selection: { ...v.selection, left, top, width, height, visible: true },
|
|
14
|
+
crop: { imagecropleft: cropLeft, imagecroptop: cropTop, imagecropright: cropRight, imagecropbottom: cropBottom }
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
setCrop(cropLeft, cropTop, cropRight, cropBottom, imageWidth, imageHeight) {
|
|
18
|
+
// calculate values for left, top, width, height
|
|
19
|
+
const left = Math.round(cropLeft * imageWidth);
|
|
20
|
+
const top = Math.round(cropTop * imageHeight);
|
|
21
|
+
const cr = cropRight <= 0 ? 1 : cropRight;
|
|
22
|
+
const width = imageWidth * cr - left;
|
|
23
|
+
const cb = cropBottom <= 0 ? 1 : cropBottom;
|
|
24
|
+
const height = imageHeight * cb - top;
|
|
25
|
+
this.update(v => ({
|
|
26
|
+
...v,
|
|
27
|
+
selection: { ...v.selection, left, top, width, height, visible: true, shieldVisible: true },
|
|
28
|
+
crop: { imagecropleft: cropLeft, imagecroptop: cropTop, imagecropright: cropRight, imagecropbottom: cropBottom }
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
setTrack(track) {
|
|
32
|
+
this.update(v => ({ ...v, selection: { ...v.selection, track } }));
|
|
33
|
+
}
|
|
34
|
+
setShieldVisibility(vis) {
|
|
35
|
+
this.update(v => ({ ...v, selection: { ...v.selection, shieldVisible: vis } }));
|
|
36
|
+
}
|
|
37
|
+
setDrag(drag) {
|
|
38
|
+
this.update(v => ({ ...v, selection: { ...v.selection, drag } }));
|
|
39
|
+
}
|
|
40
|
+
setSelectionVisibility(visible) {
|
|
41
|
+
this.update(v => ({ ...v, selection: { ...v.selection, visible } }));
|
|
42
|
+
}
|
|
43
|
+
moveSelectionTo(left, top, imageWidth, imageHeight) {
|
|
44
|
+
// calculate crop values using new left and right, current selection width and height
|
|
45
|
+
const cropLeft = left / imageWidth;
|
|
46
|
+
const cropTop = top / imageHeight;
|
|
47
|
+
const cropRight = (left + this.value.selection.width) / imageWidth;
|
|
48
|
+
const cropBottom = (top + this.value.selection.height) / imageHeight;
|
|
49
|
+
this.update(v => ({
|
|
50
|
+
...v,
|
|
51
|
+
selection: { ...v.selection, left, top },
|
|
52
|
+
crop: { imagecropleft: cropLeft, imagecroptop: cropTop, imagecropright: cropRight, imagecropbottom: cropBottom }
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
maximize(imageWidth, imageHeight, selectionAspectRatio) {
|
|
56
|
+
let cropLeft, cropRight, cropTop, cropBottom;
|
|
57
|
+
const imageAspectRatio = imageWidth / imageHeight;
|
|
58
|
+
if (imageAspectRatio > selectionAspectRatio) {
|
|
59
|
+
const targetWidth = selectionAspectRatio * imageHeight;
|
|
60
|
+
cropLeft = (Math.round(imageWidth - targetWidth) / 2) / imageWidth;
|
|
61
|
+
cropTop = 0;
|
|
62
|
+
cropRight = ((Math.round(imageWidth - targetWidth) / 2) + targetWidth) / imageWidth;
|
|
63
|
+
cropBottom = 1;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const targetHeight = imageWidth / selectionAspectRatio;
|
|
67
|
+
cropLeft = 0;
|
|
68
|
+
cropTop = (Math.round(imageHeight - targetHeight) / 2) / imageHeight;
|
|
69
|
+
cropRight = 1;
|
|
70
|
+
cropBottom = ((Math.round(imageHeight - targetHeight) / 2) + targetHeight) / imageHeight;
|
|
71
|
+
}
|
|
72
|
+
this.setCrop(cropLeft, cropTop, cropRight, cropBottom, imageWidth, imageHeight);
|
|
73
|
+
this.setSelectionVisibility(true);
|
|
74
|
+
this.setShieldVisibility(true);
|
|
75
|
+
}
|
|
76
|
+
reset() {
|
|
77
|
+
this.update(v => ({
|
|
78
|
+
selection: {
|
|
79
|
+
left: undefined,
|
|
80
|
+
top: undefined,
|
|
81
|
+
width: undefined,
|
|
82
|
+
height: undefined,
|
|
83
|
+
visible: false,
|
|
84
|
+
shieldVisible: false,
|
|
85
|
+
track: false,
|
|
86
|
+
drag: false
|
|
87
|
+
},
|
|
88
|
+
crop: {
|
|
89
|
+
imagecropbottom: 0,
|
|
90
|
+
imagecropleft: 0,
|
|
91
|
+
imagecropright: 0,
|
|
92
|
+
imagecroptop: 0
|
|
93
|
+
}
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ImageCropperOutput {
|
|
2
|
+
imagecropleft: number;
|
|
3
|
+
imagecropright: number;
|
|
4
|
+
imagecroptop: number;
|
|
5
|
+
imagecropbottom: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ICropSelection {
|
|
8
|
+
left: number | undefined;
|
|
9
|
+
top: number | undefined;
|
|
10
|
+
width: number | undefined;
|
|
11
|
+
height: number | undefined;
|
|
12
|
+
visible: boolean;
|
|
13
|
+
shieldVisible: boolean;
|
|
14
|
+
track: boolean;
|
|
15
|
+
drag: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ICropperStore {
|
|
18
|
+
selection: ICropSelection;
|
|
19
|
+
crop: ImageCropperOutput;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FieldImageCropper } from './FieldImageCropper.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FieldImageCropper } from './FieldImageCropper.svelte';
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dosgato/dialog",
|
|
3
3
|
"description": "A component library for building forms that edit a JSON document.",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.32",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@txstate-mws/svelte-components": "^1.3.5",
|
|
7
7
|
"@txstate-mws/svelte-forms": "^1.2.1",
|
|
@@ -80,6 +80,10 @@
|
|
|
80
80
|
"./iconpicker/FieldIconPicker.svelte": "./iconpicker/FieldIconPicker.svelte",
|
|
81
81
|
"./iconpicker/iconpicker": "./iconpicker/iconpicker.js",
|
|
82
82
|
"./iconpicker": "./iconpicker/index.js",
|
|
83
|
+
"./imagecropper/FieldImageCropper.svelte": "./imagecropper/FieldImageCropper.svelte",
|
|
84
|
+
"./imagecropper/ImageCropperStore": "./imagecropper/ImageCropperStore.js",
|
|
85
|
+
"./imagecropper/imagecropper": "./imagecropper/imagecropper.js",
|
|
86
|
+
"./imagecropper": "./imagecropper/index.js",
|
|
83
87
|
".": "./index.js",
|
|
84
88
|
"./tree/LoadIcon.svelte": "./tree/LoadIcon.svelte",
|
|
85
89
|
"./tree/Tree.svelte": "./tree/Tree.svelte",
|