@muxima-ui/core 1.1.0 → 1.3.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/README.md +15 -0
- package/fesm2022/muxima-ui-core-carousel.mjs +377 -0
- package/fesm2022/muxima-ui-core-carousel.mjs.map +1 -0
- package/fesm2022/muxima-ui-core-color-picker.mjs +755 -0
- package/fesm2022/muxima-ui-core-color-picker.mjs.map +1 -0
- package/fesm2022/muxima-ui-core-data-grid.mjs +1187 -0
- package/fesm2022/muxima-ui-core-data-grid.mjs.map +1 -0
- package/fesm2022/muxima-ui-core-editor.mjs +575 -0
- package/fesm2022/muxima-ui-core-editor.mjs.map +1 -0
- package/fesm2022/muxima-ui-core-image-cropper.mjs +568 -0
- package/fesm2022/muxima-ui-core-image-cropper.mjs.map +1 -0
- package/fesm2022/muxima-ui-core-kanban.mjs +369 -0
- package/fesm2022/muxima-ui-core-kanban.mjs.map +1 -0
- package/fesm2022/muxima-ui-core-notification-center.mjs +487 -0
- package/fesm2022/muxima-ui-core-notification-center.mjs.map +1 -0
- package/fesm2022/muxima-ui-core-sidebar.mjs +333 -0
- package/fesm2022/muxima-ui-core-sidebar.mjs.map +1 -0
- package/fesm2022/muxima-ui-core-timeline.mjs +117 -0
- package/fesm2022/muxima-ui-core-timeline.mjs.map +1 -0
- package/fesm2022/muxima-ui-core.mjs +1 -1
- package/fesm2022/muxima-ui-core.mjs.map +1 -1
- package/package.json +57 -2
- package/styles/base.css +17 -0
- package/styles/components/carousel.css +113 -0
- package/styles/components/color-picker.css +207 -0
- package/styles/components/data-grid.css +235 -0
- package/styles/components/editor.css +187 -0
- package/styles/components/image-cropper.css +113 -0
- package/styles/components/kanban.css +115 -0
- package/styles/components/notification-center.css +220 -0
- package/styles/components/sidebar.css +211 -0
- package/styles/components/timeline.css +180 -0
- package/styles/muxima.css +9 -0
- package/types/muxima-ui-core-carousel.d.ts +72 -0
- package/types/muxima-ui-core-color-picker.d.ts +153 -0
- package/types/muxima-ui-core-data-grid.d.ts +175 -0
- package/types/muxima-ui-core-editor.d.ts +104 -0
- package/types/muxima-ui-core-image-cropper.d.ts +105 -0
- package/types/muxima-ui-core-kanban.d.ts +83 -0
- package/types/muxima-ui-core-notification-center.d.ts +91 -0
- package/types/muxima-ui-core-sidebar.d.ts +115 -0
- package/types/muxima-ui-core-timeline.d.ts +59 -0
- package/types/muxima-ui-core.d.ts +1 -1
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, booleanAttribute, numberAttribute, model, output, viewChild, inject, signal, computed, effect, untracked, DestroyRef, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
4
|
+
import { mxUniqueId } from '@muxima-ui/core/form-field';
|
|
5
|
+
|
|
6
|
+
/** Mantém o rectângulo dentro dos limites, sem mudar o tamanho (a não ser que não caiba). */
|
|
7
|
+
function mxClampRect(r, bounds) {
|
|
8
|
+
const width = Math.min(r.width, bounds.width);
|
|
9
|
+
const height = Math.min(r.height, bounds.height);
|
|
10
|
+
return {
|
|
11
|
+
width,
|
|
12
|
+
height,
|
|
13
|
+
x: Math.min(Math.max(r.x, bounds.x), bounds.x + bounds.width - width),
|
|
14
|
+
y: Math.min(Math.max(r.y, bounds.y), bounds.y + bounds.height - height),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Maior rectângulo com a proporção dada, centrado nos limites (ocupa `fill` do espaço). */
|
|
18
|
+
function mxCenteredRect(bounds, ratio, fill = 0.8) {
|
|
19
|
+
let width = bounds.width * fill;
|
|
20
|
+
let height = bounds.height * fill;
|
|
21
|
+
if (ratio) {
|
|
22
|
+
if (width / height > ratio)
|
|
23
|
+
width = height * ratio;
|
|
24
|
+
else
|
|
25
|
+
height = width / ratio;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
x: bounds.x + (bounds.width - width) / 2,
|
|
29
|
+
y: bounds.y + (bounds.height - height) / 2,
|
|
30
|
+
width,
|
|
31
|
+
height,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Redimensiona a partir de um puxador por (dx, dy), respeitando a proporção, o tamanho
|
|
36
|
+
* mínimo e os limites. O lado oposto ao puxador fica parado.
|
|
37
|
+
*/
|
|
38
|
+
function mxResizeRect(r, handle, dx, dy, bounds, ratio, min = 24) {
|
|
39
|
+
let { x, y, width, height } = r;
|
|
40
|
+
const right = x + width;
|
|
41
|
+
const bottom = y + height;
|
|
42
|
+
if (handle.includes('e'))
|
|
43
|
+
width = Math.min(Math.max(width + dx, min), bounds.x + bounds.width - x);
|
|
44
|
+
if (handle.includes('w')) {
|
|
45
|
+
const nx = Math.min(Math.max(x + dx, bounds.x), right - min);
|
|
46
|
+
width = right - nx;
|
|
47
|
+
x = nx;
|
|
48
|
+
}
|
|
49
|
+
if (handle.includes('s'))
|
|
50
|
+
height = Math.min(Math.max(height + dy, min), bounds.y + bounds.height - y);
|
|
51
|
+
if (handle.includes('n')) {
|
|
52
|
+
const ny = Math.min(Math.max(y + dy, bounds.y), bottom - min);
|
|
53
|
+
height = bottom - ny;
|
|
54
|
+
y = ny;
|
|
55
|
+
}
|
|
56
|
+
if (ratio) {
|
|
57
|
+
// o eixo que o puxador controla manda; nos cantos, o maior movimento
|
|
58
|
+
const horizontal = handle === 'e' || handle === 'w' || (handle.length === 2 && Math.abs(dx) >= Math.abs(dy));
|
|
59
|
+
if (horizontal)
|
|
60
|
+
height = width / ratio;
|
|
61
|
+
else
|
|
62
|
+
width = height * ratio;
|
|
63
|
+
// não sair dos limites: reduz ambos
|
|
64
|
+
const maxW = handle.includes('w') ? right - bounds.x : bounds.x + bounds.width - x;
|
|
65
|
+
const maxH = handle.includes('n') ? bottom - bounds.y : bounds.y + bounds.height - y;
|
|
66
|
+
const k = Math.min(1, maxW / width, maxH / height);
|
|
67
|
+
width *= k;
|
|
68
|
+
height *= k;
|
|
69
|
+
if (handle.includes('w'))
|
|
70
|
+
x = right - width;
|
|
71
|
+
if (handle.includes('n'))
|
|
72
|
+
y = bottom - height;
|
|
73
|
+
if (handle === 'e' || handle === 'w')
|
|
74
|
+
y = Math.min(Math.max(r.y + (r.height - height) / 2, bounds.y), bounds.y + bounds.height - height);
|
|
75
|
+
if (handle === 'n' || handle === 's')
|
|
76
|
+
x = Math.min(Math.max(r.x + (r.width - width) / 2, bounds.x), bounds.x + bounds.width - width);
|
|
77
|
+
}
|
|
78
|
+
return { x, y, width, height };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const HANDLES = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
|
|
82
|
+
/**
|
|
83
|
+
* Recortar uma imagem antes de a enviar: arrastar a área, redimensionar pelos cantos,
|
|
84
|
+
* zoom e rodar. Com teclado: setas movem a área, Alt + setas mudam o tamanho (Shift para
|
|
85
|
+
* passos maiores); cada mudança é anunciada.
|
|
86
|
+
*
|
|
87
|
+
* ```html
|
|
88
|
+
* <mx-image-cropper #c [src]="ficheiro" [aspectRatio]="1" round />
|
|
89
|
+
* <button mxButton (click)="c.toBlob().then(enviar)">Guardar</button>
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* `src` aceita um URL ou um File. `toBlob()` devolve a imagem recortada na resolução original.
|
|
93
|
+
*/
|
|
94
|
+
class MxImageCropper {
|
|
95
|
+
src = input(null, ...(ngDevMode ? [{ debugName: "src" }] : /* istanbul ignore next */ []));
|
|
96
|
+
alt = input('Imagem a recortar', ...(ngDevMode ? [{ debugName: "alt" }] : /* istanbul ignore next */ []));
|
|
97
|
+
/** Largura / altura da área (1 = quadrado, 16/9...). null = livre. */
|
|
98
|
+
aspectRatio = input(null, ...(ngDevMode ? [{ debugName: "aspectRatio" }] : /* istanbul ignore next */ []));
|
|
99
|
+
/** Máscara redonda (fotografias de perfil). toBlob devolve PNG com cantos transparentes. */
|
|
100
|
+
round = input(false, { ...(ngDevMode ? { debugName: "round" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
101
|
+
controls = input(true, { ...(ngDevMode ? { debugName: "controls" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
102
|
+
height = input('320px', ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
|
|
103
|
+
/** Tamanho mínimo da área no ecrã, em px. */
|
|
104
|
+
minSize = input(40, { ...(ngDevMode ? { debugName: "minSize" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
105
|
+
zoom = model(1, ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
106
|
+
rotation = model(0, ...(ngDevMode ? [{ debugName: "rotation" }] : /* istanbul ignore next */ []));
|
|
107
|
+
helpText = input('Setas movem a área. Alt e setas mudam o tamanho. Shift dá passos maiores.', ...(ngDevMode ? [{ debugName: "helpText" }] : /* istanbul ignore next */ []));
|
|
108
|
+
zoomText = input('Zoom', ...(ngDevMode ? [{ debugName: "zoomText" }] : /* istanbul ignore next */ []));
|
|
109
|
+
rotateLeftText = input('Rodar para a esquerda', ...(ngDevMode ? [{ debugName: "rotateLeftText" }] : /* istanbul ignore next */ []));
|
|
110
|
+
rotateRightText = input('Rodar para a direita', ...(ngDevMode ? [{ debugName: "rotateRightText" }] : /* istanbul ignore next */ []));
|
|
111
|
+
resetText = input('Repor', ...(ngDevMode ? [{ debugName: "resetText" }] : /* istanbul ignore next */ []));
|
|
112
|
+
/** Área recortada em píxeis da imagem original. */
|
|
113
|
+
areaChange = output();
|
|
114
|
+
stage = viewChild.required('stage');
|
|
115
|
+
img = viewChild('img', ...(ngDevMode ? [{ debugName: "img" }] : /* istanbul ignore next */ []));
|
|
116
|
+
announcer = inject(LiveAnnouncer);
|
|
117
|
+
helpId = mxUniqueId('mx-cropper-help');
|
|
118
|
+
handles = HANDLES;
|
|
119
|
+
url = signal(null, ...(ngDevMode ? [{ debugName: "url" }] : /* istanbul ignore next */ []));
|
|
120
|
+
natural = signal({ w: 0, h: 0 }, ...(ngDevMode ? [{ debugName: "natural" }] : /* istanbul ignore next */ []));
|
|
121
|
+
stageSize = signal({ w: 0, h: 0 }, ...(ngDevMode ? [{ debugName: "stageSize" }] : /* istanbul ignore next */ []));
|
|
122
|
+
ready = signal(false, ...(ngDevMode ? [{ debugName: "ready" }] : /* istanbul ignore next */ []));
|
|
123
|
+
box = signal({ x: 0, y: 0, width: 0, height: 0 }, ...(ngDevMode ? [{ debugName: "box" }] : /* istanbul ignore next */ []));
|
|
124
|
+
live = signal('', ...(ngDevMode ? [{ debugName: "live" }] : /* istanbul ignore next */ []));
|
|
125
|
+
objectUrl = null;
|
|
126
|
+
liveTimer;
|
|
127
|
+
/** Escala que faz a imagem caber no palco, vezes o zoom. */
|
|
128
|
+
fit = computed(() => {
|
|
129
|
+
const { w, h } = this.natural();
|
|
130
|
+
const s = this.stageSize();
|
|
131
|
+
if (!w || !h || !s.w || !s.h)
|
|
132
|
+
return 1;
|
|
133
|
+
const turned = Math.abs(this.rotation()) % 180 === 90;
|
|
134
|
+
const iw = turned ? h : w;
|
|
135
|
+
const ih = turned ? w : h;
|
|
136
|
+
return Math.min(s.w / iw, s.h / ih);
|
|
137
|
+
}, ...(ngDevMode ? [{ debugName: "fit" }] : /* istanbul ignore next */ []));
|
|
138
|
+
scale = computed(() => this.fit() * this.zoom(), ...(ngDevMode ? [{ debugName: "scale" }] : /* istanbul ignore next */ []));
|
|
139
|
+
zoomPct = computed(() => Math.round(this.zoom() * 100), ...(ngDevMode ? [{ debugName: "zoomPct" }] : /* istanbul ignore next */ []));
|
|
140
|
+
/** Rectângulo da imagem no palco (onde a área pode estar). */
|
|
141
|
+
bounds = computed(() => {
|
|
142
|
+
const { w, h } = this.natural();
|
|
143
|
+
const s = this.stageSize();
|
|
144
|
+
const turned = Math.abs(this.rotation()) % 180 === 90;
|
|
145
|
+
const iw = (turned ? h : w) * this.scale();
|
|
146
|
+
const ih = (turned ? w : h) * this.scale();
|
|
147
|
+
const bw = Math.min(iw, s.w);
|
|
148
|
+
const bh = Math.min(ih, s.h);
|
|
149
|
+
return { x: (s.w - bw) / 2, y: (s.h - bh) / 2, width: bw, height: bh };
|
|
150
|
+
}, ...(ngDevMode ? [{ debugName: "bounds" }] : /* istanbul ignore next */ []));
|
|
151
|
+
/** Área em píxeis da imagem (rodada). */
|
|
152
|
+
area = computed(() => {
|
|
153
|
+
const b = this.box();
|
|
154
|
+
const s = this.stageSize();
|
|
155
|
+
const k = 1 / this.scale();
|
|
156
|
+
const turned = Math.abs(this.rotation()) % 180 === 90;
|
|
157
|
+
const iw = (turned ? this.natural().h : this.natural().w) * this.scale();
|
|
158
|
+
const ih = (turned ? this.natural().w : this.natural().h) * this.scale();
|
|
159
|
+
const ox = (s.w - iw) / 2;
|
|
160
|
+
const oy = (s.h - ih) / 2;
|
|
161
|
+
return {
|
|
162
|
+
x: Math.round((b.x - ox) * k),
|
|
163
|
+
y: Math.round((b.y - oy) * k),
|
|
164
|
+
width: Math.round(b.width * k),
|
|
165
|
+
height: Math.round(b.height * k),
|
|
166
|
+
rotation: this.rotation(),
|
|
167
|
+
};
|
|
168
|
+
}, ...(ngDevMode ? [{ debugName: "area" }] : /* istanbul ignore next */ []));
|
|
169
|
+
boxLabel = computed(() => {
|
|
170
|
+
const a = this.area();
|
|
171
|
+
return `${a.width} por ${a.height} píxeis`;
|
|
172
|
+
}, ...(ngDevMode ? [{ debugName: "boxLabel" }] : /* istanbul ignore next */ []));
|
|
173
|
+
constructor() {
|
|
174
|
+
effect(() => {
|
|
175
|
+
const src = this.src();
|
|
176
|
+
this.ready.set(false);
|
|
177
|
+
if (this.objectUrl)
|
|
178
|
+
URL.revokeObjectURL(this.objectUrl);
|
|
179
|
+
this.objectUrl = null;
|
|
180
|
+
if (!src) {
|
|
181
|
+
this.url.set(null);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (typeof src === 'string')
|
|
185
|
+
this.url.set(src);
|
|
186
|
+
else {
|
|
187
|
+
this.objectUrl = URL.createObjectURL(src);
|
|
188
|
+
this.url.set(this.objectUrl);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
// mantém a área dentro da imagem quando o zoom ou a rotação mudam
|
|
192
|
+
effect(() => {
|
|
193
|
+
const b = this.bounds();
|
|
194
|
+
if (!this.ready())
|
|
195
|
+
return;
|
|
196
|
+
const current = untracked(() => this.box());
|
|
197
|
+
const next = mxClampRect(current, b);
|
|
198
|
+
if (next.x !== current.x ||
|
|
199
|
+
next.y !== current.y ||
|
|
200
|
+
next.width !== current.width ||
|
|
201
|
+
next.height !== current.height) {
|
|
202
|
+
untracked(() => this.box.set(next));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
effect(() => {
|
|
206
|
+
if (this.ready())
|
|
207
|
+
this.areaChange.emit(this.area());
|
|
208
|
+
});
|
|
209
|
+
const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(() => this.measure()) : null;
|
|
210
|
+
queueMicrotask(() => ro?.observe(this.stage().nativeElement));
|
|
211
|
+
inject(DestroyRef).onDestroy(() => {
|
|
212
|
+
ro?.disconnect();
|
|
213
|
+
if (this.objectUrl)
|
|
214
|
+
URL.revokeObjectURL(this.objectUrl);
|
|
215
|
+
clearTimeout(this.liveTimer);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
measure() {
|
|
219
|
+
const r = this.stage().nativeElement.getBoundingClientRect();
|
|
220
|
+
this.stageSize.set({ w: r.width, h: r.height });
|
|
221
|
+
}
|
|
222
|
+
onLoad() {
|
|
223
|
+
const el = this.img().nativeElement;
|
|
224
|
+
this.natural.set({ w: el.naturalWidth, h: el.naturalHeight });
|
|
225
|
+
this.measure();
|
|
226
|
+
this.ready.set(true);
|
|
227
|
+
this.reset();
|
|
228
|
+
}
|
|
229
|
+
/** Volta ao zoom 1, sem rotação, área centrada. */
|
|
230
|
+
reset() {
|
|
231
|
+
this.zoom.set(1);
|
|
232
|
+
this.rotation.set(0);
|
|
233
|
+
queueMicrotask(() => this.box.set(mxCenteredRect(this.bounds(), this.aspectRatio())));
|
|
234
|
+
}
|
|
235
|
+
setZoom(z) {
|
|
236
|
+
const prev = this.box();
|
|
237
|
+
this.zoom.set(Math.min(Math.max(z, 1), 4));
|
|
238
|
+
this.box.set(mxClampRect(prev, this.bounds()));
|
|
239
|
+
}
|
|
240
|
+
rotate(deg) {
|
|
241
|
+
this.rotation.update((r) => (r + deg + 360) % 360);
|
|
242
|
+
this.box.set(mxCenteredRect(this.bounds(), this.aspectRatio()));
|
|
243
|
+
this.say(`Rodada ${this.rotation()} graus`);
|
|
244
|
+
}
|
|
245
|
+
say(text) {
|
|
246
|
+
clearTimeout(this.liveTimer);
|
|
247
|
+
this.liveTimer = setTimeout(() => this.live.set(text), 250);
|
|
248
|
+
}
|
|
249
|
+
onKeydown(e) {
|
|
250
|
+
const step = e.shiftKey ? 10 : 1;
|
|
251
|
+
const d = {
|
|
252
|
+
ArrowLeft: [-step, 0],
|
|
253
|
+
ArrowRight: [step, 0],
|
|
254
|
+
ArrowUp: [0, -step],
|
|
255
|
+
ArrowDown: [0, step],
|
|
256
|
+
};
|
|
257
|
+
const m = d[e.key];
|
|
258
|
+
if (!m)
|
|
259
|
+
return;
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
const b = this.box();
|
|
262
|
+
if (e.altKey) {
|
|
263
|
+
// Alt: muda o tamanho a partir do canto inferior direito
|
|
264
|
+
this.box.set(mxResizeRect(b, 'se', m[0], m[1] || (this.aspectRatio() ? m[0] / this.aspectRatio() : 0), this.bounds(), this.aspectRatio(), this.minSize()));
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
this.box.set(mxClampRect({ ...b, x: b.x + m[0], y: b.y + m[1] }, this.bounds()));
|
|
268
|
+
}
|
|
269
|
+
const a = this.area();
|
|
270
|
+
this.say(`${a.width} por ${a.height} píxeis, a ${a.x} da esquerda e ${a.y} do topo`);
|
|
271
|
+
}
|
|
272
|
+
drag(e, apply) {
|
|
273
|
+
if (e.button !== 0)
|
|
274
|
+
return;
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
e.stopPropagation();
|
|
277
|
+
const start = this.box();
|
|
278
|
+
const sx = e.clientX;
|
|
279
|
+
const sy = e.clientY;
|
|
280
|
+
const target = e.currentTarget;
|
|
281
|
+
target.setPointerCapture?.(e.pointerId);
|
|
282
|
+
const move = (ev) => this.box.set(apply(ev.clientX - sx, ev.clientY - sy, start));
|
|
283
|
+
const up = () => {
|
|
284
|
+
target.removeEventListener('pointermove', move);
|
|
285
|
+
target.removeEventListener('pointerup', up);
|
|
286
|
+
};
|
|
287
|
+
target.addEventListener('pointermove', move);
|
|
288
|
+
target.addEventListener('pointerup', up);
|
|
289
|
+
}
|
|
290
|
+
startMove(e) {
|
|
291
|
+
e.currentTarget.focus();
|
|
292
|
+
this.drag(e, (dx, dy, s) => mxClampRect({ ...s, x: s.x + dx, y: s.y + dy }, this.bounds()));
|
|
293
|
+
}
|
|
294
|
+
startResize(e, h) {
|
|
295
|
+
this.drag(e, (dx, dy, s) => mxResizeRect(s, h, dx, dy, this.bounds(), this.aspectRatio(), this.minSize()));
|
|
296
|
+
}
|
|
297
|
+
/** Desenha o recorte num canvas na resolução original. */
|
|
298
|
+
toCanvas() {
|
|
299
|
+
const img = this.img()?.nativeElement;
|
|
300
|
+
const canvas = document.createElement('canvas');
|
|
301
|
+
if (!img || !this.ready())
|
|
302
|
+
return canvas;
|
|
303
|
+
const b = this.box();
|
|
304
|
+
const s = this.stageSize();
|
|
305
|
+
const k = 1 / this.scale();
|
|
306
|
+
canvas.width = Math.max(1, Math.round(b.width * k));
|
|
307
|
+
canvas.height = Math.max(1, Math.round(b.height * k));
|
|
308
|
+
const ctx = canvas.getContext('2d');
|
|
309
|
+
if (!ctx)
|
|
310
|
+
return canvas;
|
|
311
|
+
if (this.round()) {
|
|
312
|
+
ctx.beginPath();
|
|
313
|
+
ctx.ellipse(canvas.width / 2, canvas.height / 2, canvas.width / 2, canvas.height / 2, 0, 0, Math.PI * 2);
|
|
314
|
+
ctx.clip();
|
|
315
|
+
}
|
|
316
|
+
ctx.scale(k, k);
|
|
317
|
+
ctx.translate(-b.x, -b.y);
|
|
318
|
+
ctx.translate(s.w / 2, s.h / 2);
|
|
319
|
+
ctx.rotate((this.rotation() * Math.PI) / 180);
|
|
320
|
+
ctx.scale(this.scale(), this.scale());
|
|
321
|
+
ctx.drawImage(img, -this.natural().w / 2, -this.natural().h / 2);
|
|
322
|
+
return canvas;
|
|
323
|
+
}
|
|
324
|
+
/** Recorte como Blob (PNG por omissão; com `round` é sempre PNG). */
|
|
325
|
+
toBlob(type = 'image/png', quality = 0.92) {
|
|
326
|
+
const canvas = this.toCanvas();
|
|
327
|
+
return new Promise((resolve) => canvas.toBlob(resolve, this.round() ? 'image/png' : type, quality));
|
|
328
|
+
}
|
|
329
|
+
toDataURL(type = 'image/png', quality = 0.92) {
|
|
330
|
+
return this.toCanvas().toDataURL(this.round() ? 'image/png' : type, quality);
|
|
331
|
+
}
|
|
332
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: MxImageCropper, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
333
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.25", type: MxImageCropper, isStandalone: true, selector: "mx-image-cropper", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, round: { classPropertyName: "round", publicName: "round", isSignal: true, isRequired: false, transformFunction: null }, controls: { classPropertyName: "controls", publicName: "controls", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, minSize: { classPropertyName: "minSize", publicName: "minSize", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, rotation: { classPropertyName: "rotation", publicName: "rotation", isSignal: true, isRequired: false, transformFunction: null }, helpText: { classPropertyName: "helpText", publicName: "helpText", isSignal: true, isRequired: false, transformFunction: null }, zoomText: { classPropertyName: "zoomText", publicName: "zoomText", isSignal: true, isRequired: false, transformFunction: null }, rotateLeftText: { classPropertyName: "rotateLeftText", publicName: "rotateLeftText", isSignal: true, isRequired: false, transformFunction: null }, rotateRightText: { classPropertyName: "rotateRightText", publicName: "rotateRightText", isSignal: true, isRequired: false, transformFunction: null }, resetText: { classPropertyName: "resetText", publicName: "resetText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { zoom: "zoomChange", rotation: "rotationChange", areaChange: "areaChange" }, host: { properties: { "attr.data-round": "round() ? \"\" : null", "attr.aria-label": "null" }, classAttribute: "mx-image-cropper" }, viewQueries: [{ propertyName: "stage", first: true, predicate: ["stage"], descendants: true, isSignal: true }, { propertyName: "img", first: true, predicate: ["img"], descendants: true, isSignal: true }], exportAs: ["mxImageCropper"], ngImport: i0, template: `
|
|
334
|
+
<div class="mx-cropper-stage" #stage [style.height]="height()">
|
|
335
|
+
@if (url()) {
|
|
336
|
+
<img
|
|
337
|
+
#img
|
|
338
|
+
class="mx-cropper-image"
|
|
339
|
+
[src]="url()"
|
|
340
|
+
[alt]="alt()"
|
|
341
|
+
draggable="false"
|
|
342
|
+
[style.width.px]="natural().w"
|
|
343
|
+
[style.height.px]="natural().h"
|
|
344
|
+
[style.transform]="
|
|
345
|
+
'translate(-50%, -50%) rotate(' + rotation() + 'deg) scale(' + scale() + ')'
|
|
346
|
+
"
|
|
347
|
+
(load)="onLoad()"
|
|
348
|
+
/>
|
|
349
|
+
}
|
|
350
|
+
@if (ready()) {
|
|
351
|
+
<div
|
|
352
|
+
class="mx-cropper-box"
|
|
353
|
+
role="group"
|
|
354
|
+
tabindex="0"
|
|
355
|
+
aria-roledescription="área de recorte"
|
|
356
|
+
[attr.aria-label]="boxLabel()"
|
|
357
|
+
[attr.aria-describedby]="helpId"
|
|
358
|
+
[style.left.px]="box().x"
|
|
359
|
+
[style.top.px]="box().y"
|
|
360
|
+
[style.width.px]="box().width"
|
|
361
|
+
[style.height.px]="box().height"
|
|
362
|
+
(pointerdown)="startMove($event)"
|
|
363
|
+
(keydown)="onKeydown($event)"
|
|
364
|
+
>
|
|
365
|
+
<span class="mx-cropper-grid" aria-hidden="true"></span>
|
|
366
|
+
@for (h of handles; track h) {
|
|
367
|
+
<span
|
|
368
|
+
class="mx-cropper-handle"
|
|
369
|
+
[attr.data-handle]="h"
|
|
370
|
+
aria-hidden="true"
|
|
371
|
+
(pointerdown)="startResize($event, h)"
|
|
372
|
+
></span>
|
|
373
|
+
}
|
|
374
|
+
</div>
|
|
375
|
+
}
|
|
376
|
+
</div>
|
|
377
|
+
<p class="mx-visually-hidden" [id]="helpId">{{ helpText() }}</p>
|
|
378
|
+
@if (controls()) {
|
|
379
|
+
<div class="mx-cropper-controls">
|
|
380
|
+
<label class="mx-cropper-zoom">
|
|
381
|
+
<span>{{ zoomText() }}</span>
|
|
382
|
+
<input
|
|
383
|
+
type="range"
|
|
384
|
+
class="mx-slider"
|
|
385
|
+
min="1"
|
|
386
|
+
max="4"
|
|
387
|
+
step="0.05"
|
|
388
|
+
[value]="zoom()"
|
|
389
|
+
[disabled]="!ready()"
|
|
390
|
+
[attr.aria-valuetext]="zoomPct() + '%'"
|
|
391
|
+
(input)="setZoom($any($event.target).valueAsNumber)"
|
|
392
|
+
/>
|
|
393
|
+
</label>
|
|
394
|
+
<button
|
|
395
|
+
type="button"
|
|
396
|
+
class="mx-cropper-button"
|
|
397
|
+
[disabled]="!ready()"
|
|
398
|
+
[attr.aria-label]="rotateLeftText()"
|
|
399
|
+
(click)="rotate(-90)"
|
|
400
|
+
>
|
|
401
|
+
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
402
|
+
<path
|
|
403
|
+
d="M3.5 6.5a5 5 0 1 1 .8 5.2M3.5 2.5v4h4"
|
|
404
|
+
stroke="currentColor"
|
|
405
|
+
stroke-width="1.4"
|
|
406
|
+
stroke-linecap="round"
|
|
407
|
+
stroke-linejoin="round"
|
|
408
|
+
/>
|
|
409
|
+
</svg>
|
|
410
|
+
</button>
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
class="mx-cropper-button"
|
|
414
|
+
[disabled]="!ready()"
|
|
415
|
+
[attr.aria-label]="rotateRightText()"
|
|
416
|
+
(click)="rotate(90)"
|
|
417
|
+
>
|
|
418
|
+
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
419
|
+
<path
|
|
420
|
+
d="M12.5 6.5a5 5 0 1 0-.8 5.2M12.5 2.5v4h-4"
|
|
421
|
+
stroke="currentColor"
|
|
422
|
+
stroke-width="1.4"
|
|
423
|
+
stroke-linecap="round"
|
|
424
|
+
stroke-linejoin="round"
|
|
425
|
+
/>
|
|
426
|
+
</svg>
|
|
427
|
+
</button>
|
|
428
|
+
<button
|
|
429
|
+
type="button"
|
|
430
|
+
class="mx-cropper-button mx-cropper-reset"
|
|
431
|
+
[disabled]="!ready()"
|
|
432
|
+
(click)="reset()"
|
|
433
|
+
>
|
|
434
|
+
{{ resetText() }}
|
|
435
|
+
</button>
|
|
436
|
+
</div>
|
|
437
|
+
}
|
|
438
|
+
<span class="mx-visually-hidden" aria-live="polite">{{ live() }}</span>
|
|
439
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
440
|
+
}
|
|
441
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.25", ngImport: i0, type: MxImageCropper, decorators: [{
|
|
442
|
+
type: Component,
|
|
443
|
+
args: [{
|
|
444
|
+
selector: 'mx-image-cropper',
|
|
445
|
+
exportAs: 'mxImageCropper',
|
|
446
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
447
|
+
encapsulation: ViewEncapsulation.None,
|
|
448
|
+
host: {
|
|
449
|
+
class: 'mx-image-cropper',
|
|
450
|
+
'[attr.data-round]': 'round() ? "" : null',
|
|
451
|
+
'[attr.aria-label]': 'null',
|
|
452
|
+
},
|
|
453
|
+
template: `
|
|
454
|
+
<div class="mx-cropper-stage" #stage [style.height]="height()">
|
|
455
|
+
@if (url()) {
|
|
456
|
+
<img
|
|
457
|
+
#img
|
|
458
|
+
class="mx-cropper-image"
|
|
459
|
+
[src]="url()"
|
|
460
|
+
[alt]="alt()"
|
|
461
|
+
draggable="false"
|
|
462
|
+
[style.width.px]="natural().w"
|
|
463
|
+
[style.height.px]="natural().h"
|
|
464
|
+
[style.transform]="
|
|
465
|
+
'translate(-50%, -50%) rotate(' + rotation() + 'deg) scale(' + scale() + ')'
|
|
466
|
+
"
|
|
467
|
+
(load)="onLoad()"
|
|
468
|
+
/>
|
|
469
|
+
}
|
|
470
|
+
@if (ready()) {
|
|
471
|
+
<div
|
|
472
|
+
class="mx-cropper-box"
|
|
473
|
+
role="group"
|
|
474
|
+
tabindex="0"
|
|
475
|
+
aria-roledescription="área de recorte"
|
|
476
|
+
[attr.aria-label]="boxLabel()"
|
|
477
|
+
[attr.aria-describedby]="helpId"
|
|
478
|
+
[style.left.px]="box().x"
|
|
479
|
+
[style.top.px]="box().y"
|
|
480
|
+
[style.width.px]="box().width"
|
|
481
|
+
[style.height.px]="box().height"
|
|
482
|
+
(pointerdown)="startMove($event)"
|
|
483
|
+
(keydown)="onKeydown($event)"
|
|
484
|
+
>
|
|
485
|
+
<span class="mx-cropper-grid" aria-hidden="true"></span>
|
|
486
|
+
@for (h of handles; track h) {
|
|
487
|
+
<span
|
|
488
|
+
class="mx-cropper-handle"
|
|
489
|
+
[attr.data-handle]="h"
|
|
490
|
+
aria-hidden="true"
|
|
491
|
+
(pointerdown)="startResize($event, h)"
|
|
492
|
+
></span>
|
|
493
|
+
}
|
|
494
|
+
</div>
|
|
495
|
+
}
|
|
496
|
+
</div>
|
|
497
|
+
<p class="mx-visually-hidden" [id]="helpId">{{ helpText() }}</p>
|
|
498
|
+
@if (controls()) {
|
|
499
|
+
<div class="mx-cropper-controls">
|
|
500
|
+
<label class="mx-cropper-zoom">
|
|
501
|
+
<span>{{ zoomText() }}</span>
|
|
502
|
+
<input
|
|
503
|
+
type="range"
|
|
504
|
+
class="mx-slider"
|
|
505
|
+
min="1"
|
|
506
|
+
max="4"
|
|
507
|
+
step="0.05"
|
|
508
|
+
[value]="zoom()"
|
|
509
|
+
[disabled]="!ready()"
|
|
510
|
+
[attr.aria-valuetext]="zoomPct() + '%'"
|
|
511
|
+
(input)="setZoom($any($event.target).valueAsNumber)"
|
|
512
|
+
/>
|
|
513
|
+
</label>
|
|
514
|
+
<button
|
|
515
|
+
type="button"
|
|
516
|
+
class="mx-cropper-button"
|
|
517
|
+
[disabled]="!ready()"
|
|
518
|
+
[attr.aria-label]="rotateLeftText()"
|
|
519
|
+
(click)="rotate(-90)"
|
|
520
|
+
>
|
|
521
|
+
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
522
|
+
<path
|
|
523
|
+
d="M3.5 6.5a5 5 0 1 1 .8 5.2M3.5 2.5v4h4"
|
|
524
|
+
stroke="currentColor"
|
|
525
|
+
stroke-width="1.4"
|
|
526
|
+
stroke-linecap="round"
|
|
527
|
+
stroke-linejoin="round"
|
|
528
|
+
/>
|
|
529
|
+
</svg>
|
|
530
|
+
</button>
|
|
531
|
+
<button
|
|
532
|
+
type="button"
|
|
533
|
+
class="mx-cropper-button"
|
|
534
|
+
[disabled]="!ready()"
|
|
535
|
+
[attr.aria-label]="rotateRightText()"
|
|
536
|
+
(click)="rotate(90)"
|
|
537
|
+
>
|
|
538
|
+
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
539
|
+
<path
|
|
540
|
+
d="M12.5 6.5a5 5 0 1 0-.8 5.2M12.5 2.5v4h-4"
|
|
541
|
+
stroke="currentColor"
|
|
542
|
+
stroke-width="1.4"
|
|
543
|
+
stroke-linecap="round"
|
|
544
|
+
stroke-linejoin="round"
|
|
545
|
+
/>
|
|
546
|
+
</svg>
|
|
547
|
+
</button>
|
|
548
|
+
<button
|
|
549
|
+
type="button"
|
|
550
|
+
class="mx-cropper-button mx-cropper-reset"
|
|
551
|
+
[disabled]="!ready()"
|
|
552
|
+
(click)="reset()"
|
|
553
|
+
>
|
|
554
|
+
{{ resetText() }}
|
|
555
|
+
</button>
|
|
556
|
+
</div>
|
|
557
|
+
}
|
|
558
|
+
<span class="mx-visually-hidden" aria-live="polite">{{ live() }}</span>
|
|
559
|
+
`,
|
|
560
|
+
}]
|
|
561
|
+
}], ctorParameters: () => [], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], alt: [{ type: i0.Input, args: [{ isSignal: true, alias: "alt", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], round: [{ type: i0.Input, args: [{ isSignal: true, alias: "round", required: false }] }], controls: [{ type: i0.Input, args: [{ isSignal: true, alias: "controls", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], minSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "minSize", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }, { type: i0.Output, args: ["zoomChange"] }], rotation: [{ type: i0.Input, args: [{ isSignal: true, alias: "rotation", required: false }] }, { type: i0.Output, args: ["rotationChange"] }], helpText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helpText", required: false }] }], zoomText: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomText", required: false }] }], rotateLeftText: [{ type: i0.Input, args: [{ isSignal: true, alias: "rotateLeftText", required: false }] }], rotateRightText: [{ type: i0.Input, args: [{ isSignal: true, alias: "rotateRightText", required: false }] }], resetText: [{ type: i0.Input, args: [{ isSignal: true, alias: "resetText", required: false }] }], areaChange: [{ type: i0.Output, args: ["areaChange"] }], stage: [{ type: i0.ViewChild, args: ['stage', { isSignal: true }] }], img: [{ type: i0.ViewChild, args: ['img', { isSignal: true }] }] } });
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Generated bundle index. Do not edit.
|
|
565
|
+
*/
|
|
566
|
+
|
|
567
|
+
export { MxImageCropper, mxCenteredRect, mxClampRect, mxResizeRect };
|
|
568
|
+
//# sourceMappingURL=muxima-ui-core-image-cropper.mjs.map
|