@momentumcms/admin 0.5.1 → 0.5.3
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/fesm2022/{momentumcms-admin-array-field.component-pqA3_nC8.mjs → momentumcms-admin-array-field.component-ChjP5zK9.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-array-field.component-pqA3_nC8.mjs.map → momentumcms-admin-array-field.component-ChjP5zK9.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-blocks-field.component-88TEhVm4.mjs → momentumcms-admin-blocks-field.component-Dl0FxGnf.mjs} +119 -30
- package/fesm2022/momentumcms-admin-blocks-field.component-Dl0FxGnf.mjs.map +1 -0
- package/fesm2022/{momentumcms-admin-collapsible-field.component-D5Jc8h2Q.mjs → momentumcms-admin-collapsible-field.component-CSLOT0Dp.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-collapsible-field.component-D5Jc8h2Q.mjs.map → momentumcms-admin-collapsible-field.component-CSLOT0Dp.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-global-edit.page-D4QNE5AM.mjs → momentumcms-admin-global-edit.page-DHr7Icl6.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-global-edit.page-D4QNE5AM.mjs.map → momentumcms-admin-global-edit.page-DHr7Icl6.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-group-field.component-Cenc5zMW.mjs → momentumcms-admin-group-field.component-Bofhumd5.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-group-field.component-Cenc5zMW.mjs.map → momentumcms-admin-group-field.component-Bofhumd5.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-momentumcms-admin-5WigESOC.mjs → momentumcms-admin-momentumcms-admin-D8WvqCxe.mjs} +793 -25
- package/fesm2022/momentumcms-admin-momentumcms-admin-D8WvqCxe.mjs.map +1 -0
- package/fesm2022/{momentumcms-admin-relationship-field.component-DlCdpcRy.mjs → momentumcms-admin-relationship-field.component-DSZhc5MP.mjs} +8 -3
- package/fesm2022/{momentumcms-admin-relationship-field.component-DlCdpcRy.mjs.map → momentumcms-admin-relationship-field.component-DSZhc5MP.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-rich-text-field.component-BUziCgyn.mjs → momentumcms-admin-rich-text-field.component-BIUu6NXa.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-rich-text-field.component-BUziCgyn.mjs.map → momentumcms-admin-rich-text-field.component-BIUu6NXa.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-row-field.component-fFTcYU-P.mjs → momentumcms-admin-row-field.component-DBzqzooT.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-row-field.component-fFTcYU-P.mjs.map → momentumcms-admin-row-field.component-DBzqzooT.mjs.map} +1 -1
- package/fesm2022/{momentumcms-admin-tabs-field.component-D_T_JZej.mjs → momentumcms-admin-tabs-field.component-BsnCWC5J.mjs} +2 -2
- package/fesm2022/{momentumcms-admin-tabs-field.component-D_T_JZej.mjs.map → momentumcms-admin-tabs-field.component-BsnCWC5J.mjs.map} +1 -1
- package/fesm2022/momentumcms-admin.mjs +1 -1
- package/package.json +1 -1
- package/types/momentumcms-admin.d.ts +127 -62
- package/fesm2022/momentumcms-admin-blocks-field.component-88TEhVm4.mjs.map +0 -1
- package/fesm2022/momentumcms-admin-momentumcms-admin-5WigESOC.mjs.map +0 -1
|
@@ -1134,7 +1134,7 @@ function momentumAdminRoutes(configOrOptions) {
|
|
|
1134
1134
|
// Global edit
|
|
1135
1135
|
{
|
|
1136
1136
|
path: 'globals/:slug',
|
|
1137
|
-
loadComponent: () => import('./momentumcms-admin-global-edit.page-
|
|
1137
|
+
loadComponent: () => import('./momentumcms-admin-global-edit.page-DHr7Icl6.mjs').then((m) => m.GlobalEditPage),
|
|
1138
1138
|
canDeactivate: [unsavedChangesGuard],
|
|
1139
1139
|
},
|
|
1140
1140
|
// Plugin-registered routes
|
|
@@ -4837,6 +4837,427 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
4837
4837
|
}]
|
|
4838
4838
|
}], ctorParameters: () => [], propDecorators: { fileInputRef: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], uploadConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadConfig", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], pendingFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "pendingFile", required: false }] }], isUploading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isUploading", required: false }] }], uploadProgress: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadProgress", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], existingMedia: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingMedia", required: false }] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }] } });
|
|
4839
4839
|
|
|
4840
|
+
/**
|
|
4841
|
+
* Normalize a click position within a container to a 0-1 focal point.
|
|
4842
|
+
*/
|
|
4843
|
+
function normalizeFocalPoint(click, container) {
|
|
4844
|
+
if (container.width <= 0 || container.height <= 0) {
|
|
4845
|
+
return { x: 0.5, y: 0.5 };
|
|
4846
|
+
}
|
|
4847
|
+
return clampFocalPoint({
|
|
4848
|
+
x: click.x / container.width,
|
|
4849
|
+
y: click.y / container.height,
|
|
4850
|
+
});
|
|
4851
|
+
}
|
|
4852
|
+
/**
|
|
4853
|
+
* Clamp a focal point to the valid 0-1 range on both axes.
|
|
4854
|
+
*/
|
|
4855
|
+
function clampFocalPoint(point) {
|
|
4856
|
+
return {
|
|
4857
|
+
x: Math.max(0, Math.min(1, point.x)),
|
|
4858
|
+
y: Math.max(0, Math.min(1, point.y)),
|
|
4859
|
+
};
|
|
4860
|
+
}
|
|
4861
|
+
/**
|
|
4862
|
+
* Convert a normalized focal point to a CSS `object-position` string.
|
|
4863
|
+
*
|
|
4864
|
+
* @example focalPointToCssPosition({ x: 0.25, y: 0.75 }) → '25% 75%'
|
|
4865
|
+
*/
|
|
4866
|
+
function focalPointToCssPosition(point) {
|
|
4867
|
+
const fp = point ?? { x: 0.5, y: 0.5 };
|
|
4868
|
+
return `${Math.round(fp.x * 100)}% ${Math.round(fp.y * 100)}%`;
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4871
|
+
/**
|
|
4872
|
+
* Calculate the crop region for a cover-fit preview.
|
|
4873
|
+
* Same math as the server-side crop-calculator, but for browser-side preview.
|
|
4874
|
+
*/
|
|
4875
|
+
function calculatePreviewCrop(source, target, focalPoint) {
|
|
4876
|
+
const fp = focalPoint ?? { x: 0.5, y: 0.5 };
|
|
4877
|
+
const scale = Math.max(target.width / source.width, target.height / source.height);
|
|
4878
|
+
const cropW = Math.round(target.width / scale);
|
|
4879
|
+
const cropH = Math.round(target.height / scale);
|
|
4880
|
+
const focalX = fp.x * source.width;
|
|
4881
|
+
const focalY = fp.y * source.height;
|
|
4882
|
+
let x = Math.round(focalX - cropW / 2);
|
|
4883
|
+
let y = Math.round(focalY - cropH / 2);
|
|
4884
|
+
x = Math.max(0, Math.min(x, source.width - cropW));
|
|
4885
|
+
y = Math.max(0, Math.min(y, source.height - cropH));
|
|
4886
|
+
return { x, y, width: cropW, height: cropH };
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4889
|
+
/**
|
|
4890
|
+
* Focal Point Picker Component
|
|
4891
|
+
*
|
|
4892
|
+
* Displays an image with an interactive crosshair overlay.
|
|
4893
|
+
* Click anywhere on the image to set the focal point.
|
|
4894
|
+
* Optionally shows crop preview outlines for configured image sizes.
|
|
4895
|
+
*
|
|
4896
|
+
* @example
|
|
4897
|
+
* ```html
|
|
4898
|
+
* <mcms-focal-point-picker
|
|
4899
|
+
* [imageUrl]="mediaUrl"
|
|
4900
|
+
* [focalPoint]="{ x: 0.5, y: 0.5 }"
|
|
4901
|
+
* [imageSizes]="configuredSizes"
|
|
4902
|
+
* [naturalWidth]="800"
|
|
4903
|
+
* [naturalHeight]="600"
|
|
4904
|
+
* (focalPointChange)="onFocalPointChange($event)"
|
|
4905
|
+
* />
|
|
4906
|
+
* ```
|
|
4907
|
+
*/
|
|
4908
|
+
class FocalPointPickerComponent {
|
|
4909
|
+
imageElRef = viewChild('imageEl', ...(ngDevMode ? [{ debugName: "imageElRef" }] : []));
|
|
4910
|
+
/** URL of the image to display */
|
|
4911
|
+
imageUrl = input.required(...(ngDevMode ? [{ debugName: "imageUrl" }] : []));
|
|
4912
|
+
/** Current focal point (0-1 normalized) */
|
|
4913
|
+
focalPoint = input({ x: 0.5, y: 0.5 }, ...(ngDevMode ? [{ debugName: "focalPoint" }] : []));
|
|
4914
|
+
/** Alt text for the image */
|
|
4915
|
+
alt = input('', ...(ngDevMode ? [{ debugName: "alt" }] : []));
|
|
4916
|
+
/** Natural width of the source image (for crop preview calculations) */
|
|
4917
|
+
naturalWidth = input(0, ...(ngDevMode ? [{ debugName: "naturalWidth" }] : []));
|
|
4918
|
+
/** Natural height of the source image (for crop preview calculations) */
|
|
4919
|
+
naturalHeight = input(0, ...(ngDevMode ? [{ debugName: "naturalHeight" }] : []));
|
|
4920
|
+
/** Configured image sizes for crop preview outlines */
|
|
4921
|
+
imageSizes = input([], ...(ngDevMode ? [{ debugName: "imageSizes" }] : []));
|
|
4922
|
+
/** Emitted when the focal point changes */
|
|
4923
|
+
focalPointChange = output();
|
|
4924
|
+
/** Focal point X as percentage */
|
|
4925
|
+
focalPointX = computed(() => Math.round(this.focalPoint().x * 100), ...(ngDevMode ? [{ debugName: "focalPointX" }] : []));
|
|
4926
|
+
/** Focal point Y as percentage */
|
|
4927
|
+
focalPointY = computed(() => Math.round(this.focalPoint().y * 100), ...(ngDevMode ? [{ debugName: "focalPointY" }] : []));
|
|
4928
|
+
/** CSS object-position string */
|
|
4929
|
+
cssPosition = computed(() => focalPointToCssPosition(this.focalPoint()), ...(ngDevMode ? [{ debugName: "cssPosition" }] : []));
|
|
4930
|
+
/** Human-readable position label */
|
|
4931
|
+
positionLabel = computed(() => {
|
|
4932
|
+
const fp = this.focalPoint();
|
|
4933
|
+
return `${Math.round(fp.x * 100)}% x ${Math.round(fp.y * 100)}%`;
|
|
4934
|
+
}, ...(ngDevMode ? [{ debugName: "positionLabel" }] : []));
|
|
4935
|
+
/** Crop preview data for each configured size */
|
|
4936
|
+
cropPreviews = computed(() => {
|
|
4937
|
+
const w = this.naturalWidth();
|
|
4938
|
+
const h = this.naturalHeight();
|
|
4939
|
+
const fp = this.focalPoint();
|
|
4940
|
+
const sizes = this.imageSizes();
|
|
4941
|
+
if (!w || !h || sizes.length === 0)
|
|
4942
|
+
return [];
|
|
4943
|
+
return sizes
|
|
4944
|
+
.filter((s) => s.width && s.height && s.fit === 'cover')
|
|
4945
|
+
.map((s) => {
|
|
4946
|
+
const crop = calculatePreviewCrop({ width: w, height: h }, { width: s.width ?? 0, height: s.height ?? 0 }, fp);
|
|
4947
|
+
return {
|
|
4948
|
+
name: s.name,
|
|
4949
|
+
leftPct: (crop.x / w) * 100,
|
|
4950
|
+
topPct: (crop.y / h) * 100,
|
|
4951
|
+
widthPct: (crop.width / w) * 100,
|
|
4952
|
+
heightPct: (crop.height / h) * 100,
|
|
4953
|
+
};
|
|
4954
|
+
});
|
|
4955
|
+
}, ...(ngDevMode ? [{ debugName: "cropPreviews" }] : []));
|
|
4956
|
+
/**
|
|
4957
|
+
* Handle click on the image overlay.
|
|
4958
|
+
*/
|
|
4959
|
+
onClick(event) {
|
|
4960
|
+
const el = this.imageElRef();
|
|
4961
|
+
if (!el)
|
|
4962
|
+
return;
|
|
4963
|
+
const rect = el.nativeElement.getBoundingClientRect();
|
|
4964
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
4965
|
+
return;
|
|
4966
|
+
const x = event.clientX - rect.left;
|
|
4967
|
+
const y = event.clientY - rect.top;
|
|
4968
|
+
const fp = normalizeFocalPoint({ x, y }, { width: rect.width, height: rect.height });
|
|
4969
|
+
this.focalPointChange.emit(fp);
|
|
4970
|
+
}
|
|
4971
|
+
/**
|
|
4972
|
+
* Reset focal point to center.
|
|
4973
|
+
*/
|
|
4974
|
+
onResetCenter() {
|
|
4975
|
+
this.focalPointChange.emit({ x: 0.5, y: 0.5 });
|
|
4976
|
+
}
|
|
4977
|
+
/**
|
|
4978
|
+
* Nudge focal point by a small amount (keyboard navigation).
|
|
4979
|
+
*/
|
|
4980
|
+
onNudge(dx, dy, event) {
|
|
4981
|
+
event.preventDefault();
|
|
4982
|
+
const fp = this.focalPoint();
|
|
4983
|
+
const newFp = {
|
|
4984
|
+
x: Math.max(0, Math.min(1, fp.x + dx)),
|
|
4985
|
+
y: Math.max(0, Math.min(1, fp.y + dy)),
|
|
4986
|
+
};
|
|
4987
|
+
this.focalPointChange.emit(newFp);
|
|
4988
|
+
}
|
|
4989
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FocalPointPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4990
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: FocalPointPickerComponent, isStandalone: true, selector: "mcms-focal-point-picker", inputs: { imageUrl: { classPropertyName: "imageUrl", publicName: "imageUrl", isSignal: true, isRequired: true, transformFunction: null }, focalPoint: { classPropertyName: "focalPoint", publicName: "focalPoint", isSignal: true, isRequired: false, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, naturalWidth: { classPropertyName: "naturalWidth", publicName: "naturalWidth", isSignal: true, isRequired: false, transformFunction: null }, naturalHeight: { classPropertyName: "naturalHeight", publicName: "naturalHeight", isSignal: true, isRequired: false, transformFunction: null }, imageSizes: { classPropertyName: "imageSizes", publicName: "imageSizes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { focalPointChange: "focalPointChange" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "imageElRef", first: true, predicate: ["imageEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
4991
|
+
<div class="space-y-3">
|
|
4992
|
+
<div class="relative inline-block overflow-hidden rounded-lg border border-mcms-border">
|
|
4993
|
+
<!-- Image -->
|
|
4994
|
+
<img
|
|
4995
|
+
#imageEl
|
|
4996
|
+
[src]="imageUrl()"
|
|
4997
|
+
[alt]="alt() || 'Image with focal point selector'"
|
|
4998
|
+
class="block max-h-96 max-w-full"
|
|
4999
|
+
[style.object-position]="cssPosition()"
|
|
5000
|
+
/>
|
|
5001
|
+
|
|
5002
|
+
<!-- Clickable overlay -->
|
|
5003
|
+
<div
|
|
5004
|
+
class="absolute inset-0 cursor-crosshair"
|
|
5005
|
+
role="button"
|
|
5006
|
+
tabindex="0"
|
|
5007
|
+
[attr.aria-label]="'Set focal point. Current position: ' + positionLabel()"
|
|
5008
|
+
(click)="onClick($event)"
|
|
5009
|
+
(keydown.enter)="onResetCenter()"
|
|
5010
|
+
(keydown.space)="onResetCenter()"
|
|
5011
|
+
(keydown.ArrowLeft)="onNudge(-0.05, 0, $event)"
|
|
5012
|
+
(keydown.ArrowRight)="onNudge(0.05, 0, $event)"
|
|
5013
|
+
(keydown.ArrowUp)="onNudge(0, -0.05, $event)"
|
|
5014
|
+
(keydown.ArrowDown)="onNudge(0, 0.05, $event)"
|
|
5015
|
+
>
|
|
5016
|
+
<!-- Crosshair lines -->
|
|
5017
|
+
<div
|
|
5018
|
+
class="pointer-events-none absolute h-px w-full bg-white/70"
|
|
5019
|
+
[style.top.%]="focalPointY()"
|
|
5020
|
+
></div>
|
|
5021
|
+
<div
|
|
5022
|
+
class="pointer-events-none absolute w-px h-full bg-white/70"
|
|
5023
|
+
[style.left.%]="focalPointX()"
|
|
5024
|
+
></div>
|
|
5025
|
+
|
|
5026
|
+
<!-- Focal point dot -->
|
|
5027
|
+
<div
|
|
5028
|
+
class="pointer-events-none absolute h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-mcms-primary shadow-md"
|
|
5029
|
+
[style.left.%]="focalPointX()"
|
|
5030
|
+
[style.top.%]="focalPointY()"
|
|
5031
|
+
aria-hidden="true"
|
|
5032
|
+
></div>
|
|
5033
|
+
|
|
5034
|
+
<!-- Crop preview outlines -->
|
|
5035
|
+
@for (preview of cropPreviews(); track preview.name) {
|
|
5036
|
+
<div
|
|
5037
|
+
class="pointer-events-none absolute border border-dashed border-yellow-400/60"
|
|
5038
|
+
[style.left.%]="preview.leftPct"
|
|
5039
|
+
[style.top.%]="preview.topPct"
|
|
5040
|
+
[style.width.%]="preview.widthPct"
|
|
5041
|
+
[style.height.%]="preview.heightPct"
|
|
5042
|
+
[attr.aria-label]="'Crop preview for ' + preview.name"
|
|
5043
|
+
>
|
|
5044
|
+
<span class="absolute -top-5 left-0 text-xs text-yellow-400 drop-shadow-sm">
|
|
5045
|
+
{{ preview.name }}
|
|
5046
|
+
</span>
|
|
5047
|
+
</div>
|
|
5048
|
+
}
|
|
5049
|
+
</div>
|
|
5050
|
+
</div>
|
|
5051
|
+
|
|
5052
|
+
<!-- Coordinates display -->
|
|
5053
|
+
<p class="text-xs text-mcms-muted-foreground" aria-live="polite">
|
|
5054
|
+
Focal point: {{ positionLabel() }}
|
|
5055
|
+
<button
|
|
5056
|
+
class="ml-2 underline hover:text-mcms-foreground"
|
|
5057
|
+
type="button"
|
|
5058
|
+
(click)="onResetCenter()"
|
|
5059
|
+
aria-label="Reset focal point to center"
|
|
5060
|
+
>
|
|
5061
|
+
Reset to center
|
|
5062
|
+
</button>
|
|
5063
|
+
</p>
|
|
5064
|
+
</div>
|
|
5065
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5066
|
+
}
|
|
5067
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FocalPointPickerComponent, decorators: [{
|
|
5068
|
+
type: Component,
|
|
5069
|
+
args: [{
|
|
5070
|
+
selector: 'mcms-focal-point-picker',
|
|
5071
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
5072
|
+
host: { class: 'block' },
|
|
5073
|
+
template: `
|
|
5074
|
+
<div class="space-y-3">
|
|
5075
|
+
<div class="relative inline-block overflow-hidden rounded-lg border border-mcms-border">
|
|
5076
|
+
<!-- Image -->
|
|
5077
|
+
<img
|
|
5078
|
+
#imageEl
|
|
5079
|
+
[src]="imageUrl()"
|
|
5080
|
+
[alt]="alt() || 'Image with focal point selector'"
|
|
5081
|
+
class="block max-h-96 max-w-full"
|
|
5082
|
+
[style.object-position]="cssPosition()"
|
|
5083
|
+
/>
|
|
5084
|
+
|
|
5085
|
+
<!-- Clickable overlay -->
|
|
5086
|
+
<div
|
|
5087
|
+
class="absolute inset-0 cursor-crosshair"
|
|
5088
|
+
role="button"
|
|
5089
|
+
tabindex="0"
|
|
5090
|
+
[attr.aria-label]="'Set focal point. Current position: ' + positionLabel()"
|
|
5091
|
+
(click)="onClick($event)"
|
|
5092
|
+
(keydown.enter)="onResetCenter()"
|
|
5093
|
+
(keydown.space)="onResetCenter()"
|
|
5094
|
+
(keydown.ArrowLeft)="onNudge(-0.05, 0, $event)"
|
|
5095
|
+
(keydown.ArrowRight)="onNudge(0.05, 0, $event)"
|
|
5096
|
+
(keydown.ArrowUp)="onNudge(0, -0.05, $event)"
|
|
5097
|
+
(keydown.ArrowDown)="onNudge(0, 0.05, $event)"
|
|
5098
|
+
>
|
|
5099
|
+
<!-- Crosshair lines -->
|
|
5100
|
+
<div
|
|
5101
|
+
class="pointer-events-none absolute h-px w-full bg-white/70"
|
|
5102
|
+
[style.top.%]="focalPointY()"
|
|
5103
|
+
></div>
|
|
5104
|
+
<div
|
|
5105
|
+
class="pointer-events-none absolute w-px h-full bg-white/70"
|
|
5106
|
+
[style.left.%]="focalPointX()"
|
|
5107
|
+
></div>
|
|
5108
|
+
|
|
5109
|
+
<!-- Focal point dot -->
|
|
5110
|
+
<div
|
|
5111
|
+
class="pointer-events-none absolute h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-mcms-primary shadow-md"
|
|
5112
|
+
[style.left.%]="focalPointX()"
|
|
5113
|
+
[style.top.%]="focalPointY()"
|
|
5114
|
+
aria-hidden="true"
|
|
5115
|
+
></div>
|
|
5116
|
+
|
|
5117
|
+
<!-- Crop preview outlines -->
|
|
5118
|
+
@for (preview of cropPreviews(); track preview.name) {
|
|
5119
|
+
<div
|
|
5120
|
+
class="pointer-events-none absolute border border-dashed border-yellow-400/60"
|
|
5121
|
+
[style.left.%]="preview.leftPct"
|
|
5122
|
+
[style.top.%]="preview.topPct"
|
|
5123
|
+
[style.width.%]="preview.widthPct"
|
|
5124
|
+
[style.height.%]="preview.heightPct"
|
|
5125
|
+
[attr.aria-label]="'Crop preview for ' + preview.name"
|
|
5126
|
+
>
|
|
5127
|
+
<span class="absolute -top-5 left-0 text-xs text-yellow-400 drop-shadow-sm">
|
|
5128
|
+
{{ preview.name }}
|
|
5129
|
+
</span>
|
|
5130
|
+
</div>
|
|
5131
|
+
}
|
|
5132
|
+
</div>
|
|
5133
|
+
</div>
|
|
5134
|
+
|
|
5135
|
+
<!-- Coordinates display -->
|
|
5136
|
+
<p class="text-xs text-mcms-muted-foreground" aria-live="polite">
|
|
5137
|
+
Focal point: {{ positionLabel() }}
|
|
5138
|
+
<button
|
|
5139
|
+
class="ml-2 underline hover:text-mcms-foreground"
|
|
5140
|
+
type="button"
|
|
5141
|
+
(click)="onResetCenter()"
|
|
5142
|
+
aria-label="Reset focal point to center"
|
|
5143
|
+
>
|
|
5144
|
+
Reset to center
|
|
5145
|
+
</button>
|
|
5146
|
+
</p>
|
|
5147
|
+
</div>
|
|
5148
|
+
`,
|
|
5149
|
+
}]
|
|
5150
|
+
}], propDecorators: { imageElRef: [{ type: i0.ViewChild, args: ['imageEl', { isSignal: true }] }], imageUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "imageUrl", required: true }] }], focalPoint: [{ type: i0.Input, args: [{ isSignal: true, alias: "focalPoint", required: false }] }], alt: [{ type: i0.Input, args: [{ isSignal: true, alias: "alt", required: false }] }], naturalWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "naturalWidth", required: false }] }], naturalHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "naturalHeight", required: false }] }], imageSizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "imageSizes", required: false }] }], focalPointChange: [{ type: i0.Output, args: ["focalPointChange"] }] } });
|
|
5151
|
+
|
|
5152
|
+
/**
|
|
5153
|
+
* Displays generated image size variants as a grid of thumbnail cards.
|
|
5154
|
+
*
|
|
5155
|
+
* @example
|
|
5156
|
+
* ```html
|
|
5157
|
+
* <mcms-image-variants-display [sizes]="entity.sizes" />
|
|
5158
|
+
* ```
|
|
5159
|
+
*/
|
|
5160
|
+
class ImageVariantsDisplay {
|
|
5161
|
+
/** The sizes record from the media document (e.g., entity['sizes']) */
|
|
5162
|
+
sizes = input(null, ...(ngDevMode ? [{ debugName: "sizes" }] : []));
|
|
5163
|
+
/** Parsed variant entries */
|
|
5164
|
+
variants = computed(() => {
|
|
5165
|
+
const sizes = this.sizes();
|
|
5166
|
+
if (!sizes || typeof sizes !== 'object')
|
|
5167
|
+
return [];
|
|
5168
|
+
return Object.entries(sizes)
|
|
5169
|
+
.filter((entry) => {
|
|
5170
|
+
const v = entry[1];
|
|
5171
|
+
return v != null && typeof v === 'object' && 'width' in v && 'path' in v;
|
|
5172
|
+
})
|
|
5173
|
+
.map(([name, data]) => ({
|
|
5174
|
+
name,
|
|
5175
|
+
...data,
|
|
5176
|
+
}));
|
|
5177
|
+
}, ...(ngDevMode ? [{ debugName: "variants" }] : []));
|
|
5178
|
+
/** Build URL for a variant image */
|
|
5179
|
+
variantUrl(variant) {
|
|
5180
|
+
if (variant.url)
|
|
5181
|
+
return variant.url;
|
|
5182
|
+
if (variant.path)
|
|
5183
|
+
return `/api/media/file/${variant.path}`;
|
|
5184
|
+
return '';
|
|
5185
|
+
}
|
|
5186
|
+
/** Format file size for display */
|
|
5187
|
+
formatFileSize(bytes) {
|
|
5188
|
+
if (!bytes)
|
|
5189
|
+
return '';
|
|
5190
|
+
if (bytes >= 1024 * 1024)
|
|
5191
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
5192
|
+
if (bytes >= 1024)
|
|
5193
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
5194
|
+
return `${bytes} bytes`;
|
|
5195
|
+
}
|
|
5196
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ImageVariantsDisplay, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5197
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ImageVariantsDisplay, isStandalone: true, selector: "mcms-image-variants-display", inputs: { sizes: { classPropertyName: "sizes", publicName: "sizes", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
5198
|
+
@if (variants().length > 0) {
|
|
5199
|
+
<div class="space-y-3">
|
|
5200
|
+
<p class="text-sm font-medium">Generated Sizes</p>
|
|
5201
|
+
<div class="grid gap-3 grid-cols-2 sm:grid-cols-3">
|
|
5202
|
+
@for (variant of variants(); track variant.name) {
|
|
5203
|
+
<div class="rounded-lg border border-mcms-border bg-mcms-card overflow-hidden">
|
|
5204
|
+
<img
|
|
5205
|
+
[src]="variantUrl(variant)"
|
|
5206
|
+
[alt]="variant.name + ' variant'"
|
|
5207
|
+
class="h-24 w-full object-cover bg-mcms-muted"
|
|
5208
|
+
/>
|
|
5209
|
+
<div class="p-2">
|
|
5210
|
+
<p class="text-xs font-medium">{{ variant.name }}</p>
|
|
5211
|
+
<p class="text-xs text-mcms-muted-foreground">
|
|
5212
|
+
{{ variant.width }} × {{ variant.height }}
|
|
5213
|
+
</p>
|
|
5214
|
+
<p class="text-xs text-mcms-muted-foreground">
|
|
5215
|
+
{{ formatFileSize(variant.filesize) }}
|
|
5216
|
+
</p>
|
|
5217
|
+
</div>
|
|
5218
|
+
</div>
|
|
5219
|
+
}
|
|
5220
|
+
</div>
|
|
5221
|
+
</div>
|
|
5222
|
+
}
|
|
5223
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5224
|
+
}
|
|
5225
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ImageVariantsDisplay, decorators: [{
|
|
5226
|
+
type: Component,
|
|
5227
|
+
args: [{
|
|
5228
|
+
selector: 'mcms-image-variants-display',
|
|
5229
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
5230
|
+
host: { class: 'block' },
|
|
5231
|
+
template: `
|
|
5232
|
+
@if (variants().length > 0) {
|
|
5233
|
+
<div class="space-y-3">
|
|
5234
|
+
<p class="text-sm font-medium">Generated Sizes</p>
|
|
5235
|
+
<div class="grid gap-3 grid-cols-2 sm:grid-cols-3">
|
|
5236
|
+
@for (variant of variants(); track variant.name) {
|
|
5237
|
+
<div class="rounded-lg border border-mcms-border bg-mcms-card overflow-hidden">
|
|
5238
|
+
<img
|
|
5239
|
+
[src]="variantUrl(variant)"
|
|
5240
|
+
[alt]="variant.name + ' variant'"
|
|
5241
|
+
class="h-24 w-full object-cover bg-mcms-muted"
|
|
5242
|
+
/>
|
|
5243
|
+
<div class="p-2">
|
|
5244
|
+
<p class="text-xs font-medium">{{ variant.name }}</p>
|
|
5245
|
+
<p class="text-xs text-mcms-muted-foreground">
|
|
5246
|
+
{{ variant.width }} × {{ variant.height }}
|
|
5247
|
+
</p>
|
|
5248
|
+
<p class="text-xs text-mcms-muted-foreground">
|
|
5249
|
+
{{ formatFileSize(variant.filesize) }}
|
|
5250
|
+
</p>
|
|
5251
|
+
</div>
|
|
5252
|
+
</div>
|
|
5253
|
+
}
|
|
5254
|
+
</div>
|
|
5255
|
+
</div>
|
|
5256
|
+
}
|
|
5257
|
+
`,
|
|
5258
|
+
}]
|
|
5259
|
+
}], propDecorators: { sizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizes", required: false }] }] } });
|
|
5260
|
+
|
|
4840
5261
|
/**
|
|
4841
5262
|
* Entity Form Widget
|
|
4842
5263
|
*
|
|
@@ -4886,8 +5307,17 @@ class EntityFormWidget {
|
|
|
4886
5307
|
draftSaved = output();
|
|
4887
5308
|
/** Model signal — the single source of truth for form data */
|
|
4888
5309
|
formModel = signal({}, ...(ngDevMode ? [{ debugName: "formModel" }] : []));
|
|
4889
|
-
/**
|
|
4890
|
-
|
|
5310
|
+
/** Reactive form data that tracks Signal Forms changes for downstream consumers (e.g. live preview).
|
|
5311
|
+
* Reads the root FieldState value directly so it re-triggers when any field value changes,
|
|
5312
|
+
* unlike formModel which may keep the same object reference after in-place mutations. */
|
|
5313
|
+
formData = computed(() => {
|
|
5314
|
+
const ef = this.entityForm();
|
|
5315
|
+
if (!ef)
|
|
5316
|
+
return this.formModel();
|
|
5317
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- FieldState value is the full model
|
|
5318
|
+
const val = ef().value();
|
|
5319
|
+
return val ?? this.formModel();
|
|
5320
|
+
}, ...(ngDevMode ? [{ debugName: "formData" }] : []));
|
|
4891
5321
|
/** Signal forms tree — created once when collection is available */
|
|
4892
5322
|
entityForm = signal(null, ...(ngDevMode ? [{ debugName: "entityForm" }] : []));
|
|
4893
5323
|
/** Original data for edit mode */
|
|
@@ -4902,8 +5332,87 @@ class EntityFormWidget {
|
|
|
4902
5332
|
isUploadingFile = signal(false, ...(ngDevMode ? [{ debugName: "isUploadingFile" }] : []));
|
|
4903
5333
|
uploadFileProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadFileProgress" }] : []));
|
|
4904
5334
|
uploadFileError = signal(null, ...(ngDevMode ? [{ debugName: "uploadFileError" }] : []));
|
|
5335
|
+
/** Preview URL for the pending image file (focal point picker) */
|
|
5336
|
+
pendingFileUrl = signal(null, ...(ngDevMode ? [{ debugName: "pendingFileUrl" }] : []));
|
|
5337
|
+
/** Detected dimensions of the pending image */
|
|
5338
|
+
pendingImageDimensions = signal({
|
|
5339
|
+
width: 0,
|
|
5340
|
+
height: 0,
|
|
5341
|
+
}, ...(ngDevMode ? [{ debugName: "pendingImageDimensions" }] : []));
|
|
4905
5342
|
/** Whether the collection is an upload collection */
|
|
4906
5343
|
isUploadCol = computed(() => isUploadCollection(this.collection()), ...(ngDevMode ? [{ debugName: "isUploadCol" }] : []));
|
|
5344
|
+
/** Whether the current file (pending or existing) is an image */
|
|
5345
|
+
isImageFile = computed(() => {
|
|
5346
|
+
const file = this.pendingFile();
|
|
5347
|
+
if (file)
|
|
5348
|
+
return file.type.startsWith('image/');
|
|
5349
|
+
if (this.isUploadCol() && this.mode() !== 'create') {
|
|
5350
|
+
const mimeType = this.formModel()['mimeType'];
|
|
5351
|
+
return typeof mimeType === 'string' && mimeType.startsWith('image/');
|
|
5352
|
+
}
|
|
5353
|
+
return false;
|
|
5354
|
+
}, ...(ngDevMode ? [{ debugName: "isImageFile" }] : []));
|
|
5355
|
+
/** Image sizes from collection upload config */
|
|
5356
|
+
uploadImageSizes = computed(() => {
|
|
5357
|
+
return this.collection().upload?.imageSizes ?? [];
|
|
5358
|
+
}, ...(ngDevMode ? [{ debugName: "uploadImageSizes" }] : []));
|
|
5359
|
+
/** Image URL for the focal point picker */
|
|
5360
|
+
focalPointImageUrl = computed(() => {
|
|
5361
|
+
const fileUrl = this.pendingFileUrl();
|
|
5362
|
+
if (fileUrl)
|
|
5363
|
+
return fileUrl;
|
|
5364
|
+
const model = this.formModel();
|
|
5365
|
+
if (typeof model['url'] === 'string' && model['url'])
|
|
5366
|
+
return model['url'];
|
|
5367
|
+
if (typeof model['path'] === 'string' && model['path'])
|
|
5368
|
+
return `/api/media/file/${model['path']}`;
|
|
5369
|
+
return '';
|
|
5370
|
+
}, ...(ngDevMode ? [{ debugName: "focalPointImageUrl" }] : []));
|
|
5371
|
+
/** Current focal point value */
|
|
5372
|
+
currentFocalPoint = computed(() => {
|
|
5373
|
+
const fp = this.formModel()['focalPoint'];
|
|
5374
|
+
if (fp != null && typeof fp === 'object' && !Array.isArray(fp)) {
|
|
5375
|
+
const obj = fp; // eslint-disable-line @typescript-eslint/consistent-type-assertions
|
|
5376
|
+
const x = obj['x'];
|
|
5377
|
+
const y = obj['y'];
|
|
5378
|
+
if (typeof x === 'number' && typeof y === 'number') {
|
|
5379
|
+
return { x, y };
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5382
|
+
return { x: 0.5, y: 0.5 };
|
|
5383
|
+
}, ...(ngDevMode ? [{ debugName: "currentFocalPoint" }] : []));
|
|
5384
|
+
/** Natural image dimensions (pending file detection or existing media) */
|
|
5385
|
+
imageNaturalDimensions = computed(() => {
|
|
5386
|
+
if (this.pendingFile())
|
|
5387
|
+
return this.pendingImageDimensions();
|
|
5388
|
+
const model = this.formModel();
|
|
5389
|
+
return {
|
|
5390
|
+
width: typeof model['width'] === 'number' ? model['width'] : 0,
|
|
5391
|
+
height: typeof model['height'] === 'number' ? model['height'] : 0,
|
|
5392
|
+
};
|
|
5393
|
+
}, ...(ngDevMode ? [{ debugName: "imageNaturalDimensions" }] : []));
|
|
5394
|
+
/** Alt text for the focal point picker */
|
|
5395
|
+
focalPointAlt = computed(() => {
|
|
5396
|
+
const model = this.formModel();
|
|
5397
|
+
const alt = model['alt'];
|
|
5398
|
+
if (typeof alt === 'string' && alt)
|
|
5399
|
+
return alt;
|
|
5400
|
+
const fn = model['filename'];
|
|
5401
|
+
if (typeof fn === 'string')
|
|
5402
|
+
return fn;
|
|
5403
|
+
return '';
|
|
5404
|
+
}, ...(ngDevMode ? [{ debugName: "focalPointAlt" }] : []));
|
|
5405
|
+
/** Generated image sizes from the form model */
|
|
5406
|
+
formModelSizes = computed(() => {
|
|
5407
|
+
const sizes = this.formModel()['sizes'];
|
|
5408
|
+
if (sizes != null &&
|
|
5409
|
+
typeof sizes === 'object' &&
|
|
5410
|
+
!Array.isArray(sizes) &&
|
|
5411
|
+
Object.keys(sizes).length > 0) {
|
|
5412
|
+
return sizes; // eslint-disable-line @typescript-eslint/consistent-type-assertions
|
|
5413
|
+
}
|
|
5414
|
+
return null;
|
|
5415
|
+
}, ...(ngDevMode ? [{ debugName: "formModelSizes" }] : []));
|
|
4907
5416
|
/** Whether the form has been set up */
|
|
4908
5417
|
formCreated = false;
|
|
4909
5418
|
/** Whether the form has unsaved changes (from signal forms dirty tracking) */
|
|
@@ -5002,16 +5511,46 @@ class EntityFormWidget {
|
|
|
5002
5511
|
this.loadGlobal(gSlug);
|
|
5003
5512
|
}
|
|
5004
5513
|
else if (currentMode === 'create' || !id) {
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5514
|
+
// Guard: don't reset formModel if the user has already selected a file.
|
|
5515
|
+
// On Analog (SSR), reactive route signals may re-emit after hydration,
|
|
5516
|
+
// causing this effect to re-run and wipe user input.
|
|
5517
|
+
const hasPendingFile = untracked(() => this.pendingFile());
|
|
5518
|
+
if (!hasPendingFile) {
|
|
5519
|
+
this.formModel.set(createInitialFormData(col));
|
|
5520
|
+
const ef = this.entityForm();
|
|
5521
|
+
if (ef)
|
|
5522
|
+
ef().reset();
|
|
5523
|
+
}
|
|
5009
5524
|
}
|
|
5010
5525
|
else {
|
|
5011
5526
|
this.loadEntity(col.slug, id);
|
|
5012
5527
|
}
|
|
5013
5528
|
}
|
|
5014
5529
|
});
|
|
5530
|
+
// Manage preview URL for focal point picker and detect image dimensions
|
|
5531
|
+
effect((onCleanup) => {
|
|
5532
|
+
const file = this.pendingFile();
|
|
5533
|
+
if (file && file.type.startsWith('image/')) {
|
|
5534
|
+
const url = URL.createObjectURL(file);
|
|
5535
|
+
this.pendingFileUrl.set(url);
|
|
5536
|
+
let cancelled = false;
|
|
5537
|
+
onCleanup(() => {
|
|
5538
|
+
cancelled = true;
|
|
5539
|
+
URL.revokeObjectURL(url);
|
|
5540
|
+
});
|
|
5541
|
+
const img = new Image();
|
|
5542
|
+
img.onload = () => {
|
|
5543
|
+
if (!cancelled) {
|
|
5544
|
+
this.pendingImageDimensions.set({ width: img.naturalWidth, height: img.naturalHeight });
|
|
5545
|
+
}
|
|
5546
|
+
};
|
|
5547
|
+
img.src = url;
|
|
5548
|
+
}
|
|
5549
|
+
else {
|
|
5550
|
+
this.pendingFileUrl.set(null);
|
|
5551
|
+
this.pendingImageDimensions.set({ width: 0, height: 0 });
|
|
5552
|
+
}
|
|
5553
|
+
});
|
|
5015
5554
|
}
|
|
5016
5555
|
/**
|
|
5017
5556
|
* Get a FieldTree node for a top-level field by name.
|
|
@@ -5120,6 +5659,14 @@ class EntityFormWidget {
|
|
|
5120
5659
|
data['filesize'] = file.size;
|
|
5121
5660
|
this.formModel.set(data);
|
|
5122
5661
|
}
|
|
5662
|
+
/**
|
|
5663
|
+
* Handle focal point change from the picker.
|
|
5664
|
+
*/
|
|
5665
|
+
onFocalPointChange(fp) {
|
|
5666
|
+
const data = { ...this.formModel() };
|
|
5667
|
+
data['focalPoint'] = fp;
|
|
5668
|
+
this.formModel.set(data);
|
|
5669
|
+
}
|
|
5123
5670
|
/**
|
|
5124
5671
|
* Handle file removed from the upload zone.
|
|
5125
5672
|
*/
|
|
@@ -5395,7 +5942,7 @@ class EntityFormWidget {
|
|
|
5395
5942
|
<mcms-collection-upload-zone
|
|
5396
5943
|
[uploadConfig]="collection().upload"
|
|
5397
5944
|
[pendingFile]="pendingFile()"
|
|
5398
|
-
[existingMedia]="mode()
|
|
5945
|
+
[existingMedia]="mode() !== 'create' && !pendingFile() ? formModel() : null"
|
|
5399
5946
|
[disabled]="mode() === 'view'"
|
|
5400
5947
|
[isUploading]="isUploadingFile()"
|
|
5401
5948
|
[uploadProgress]="uploadFileProgress()"
|
|
@@ -5405,6 +5952,27 @@ class EntityFormWidget {
|
|
|
5405
5952
|
/>
|
|
5406
5953
|
}
|
|
5407
5954
|
|
|
5955
|
+
@if (isUploadCol() && isImageFile() && focalPointImageUrl()) {
|
|
5956
|
+
<div class="mb-6" [class.pointer-events-none]="mode() === 'view'">
|
|
5957
|
+
<p class="mb-2 text-sm font-medium">Focal Point</p>
|
|
5958
|
+
<mcms-focal-point-picker
|
|
5959
|
+
[imageUrl]="focalPointImageUrl()"
|
|
5960
|
+
[focalPoint]="currentFocalPoint()"
|
|
5961
|
+
[naturalWidth]="imageNaturalDimensions().width"
|
|
5962
|
+
[naturalHeight]="imageNaturalDimensions().height"
|
|
5963
|
+
[alt]="focalPointAlt()"
|
|
5964
|
+
[imageSizes]="uploadImageSizes()"
|
|
5965
|
+
(focalPointChange)="onFocalPointChange($event)"
|
|
5966
|
+
/>
|
|
5967
|
+
</div>
|
|
5968
|
+
}
|
|
5969
|
+
|
|
5970
|
+
@if (isUploadCol() && formModelSizes()) {
|
|
5971
|
+
<div class="mb-6">
|
|
5972
|
+
<mcms-image-variants-display [sizes]="formModelSizes()" />
|
|
5973
|
+
</div>
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5408
5976
|
<div class="space-y-6">
|
|
5409
5977
|
@for (field of visibleFields(); track field.name) {
|
|
5410
5978
|
<mcms-field-renderer
|
|
@@ -5473,7 +6041,7 @@ class EntityFormWidget {
|
|
|
5473
6041
|
</div>
|
|
5474
6042
|
}
|
|
5475
6043
|
</div>
|
|
5476
|
-
`, isInline: true, dependencies: [{ kind: "component", type: Card, selector: "mcms-card" }, { kind: "component", type: CardContent, selector: "mcms-card-content" }, { kind: "component", type: CardFooter, selector: "mcms-card-footer" }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: Spinner, selector: "mcms-spinner", inputs: ["size", "label", "class"] }, { kind: "component", type: Alert, selector: "mcms-alert", inputs: ["variant", "class"] }, { kind: "component", type: FieldRenderer, selector: "mcms-field-renderer", inputs: ["field", "formNode", "formTree", "formModel", "mode", "path"] }, { kind: "component", type: Breadcrumbs, selector: "mcms-breadcrumbs", inputs: ["class"] }, { kind: "component", type: BreadcrumbItem, selector: "mcms-breadcrumb-item", inputs: ["href", "current", "class"] }, { kind: "component", type: BreadcrumbSeparator, selector: "mcms-breadcrumb-separator", inputs: ["class"] }, { kind: "component", type: VersionHistoryWidget, selector: "mcms-version-history", inputs: ["collection", "documentId", "documentLabel"], outputs: ["restored"] }, { kind: "component", type: CollectionUploadZoneComponent, selector: "mcms-collection-upload-zone", inputs: ["uploadConfig", "disabled", "pendingFile", "isUploading", "uploadProgress", "error", "existingMedia"], outputs: ["fileSelected", "fileRemoved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6044
|
+
`, isInline: true, dependencies: [{ kind: "component", type: Card, selector: "mcms-card" }, { kind: "component", type: CardContent, selector: "mcms-card-content" }, { kind: "component", type: CardFooter, selector: "mcms-card-footer" }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: Spinner, selector: "mcms-spinner", inputs: ["size", "label", "class"] }, { kind: "component", type: Alert, selector: "mcms-alert", inputs: ["variant", "class"] }, { kind: "component", type: FieldRenderer, selector: "mcms-field-renderer", inputs: ["field", "formNode", "formTree", "formModel", "mode", "path"] }, { kind: "component", type: Breadcrumbs, selector: "mcms-breadcrumbs", inputs: ["class"] }, { kind: "component", type: BreadcrumbItem, selector: "mcms-breadcrumb-item", inputs: ["href", "current", "class"] }, { kind: "component", type: BreadcrumbSeparator, selector: "mcms-breadcrumb-separator", inputs: ["class"] }, { kind: "component", type: VersionHistoryWidget, selector: "mcms-version-history", inputs: ["collection", "documentId", "documentLabel"], outputs: ["restored"] }, { kind: "component", type: CollectionUploadZoneComponent, selector: "mcms-collection-upload-zone", inputs: ["uploadConfig", "disabled", "pendingFile", "isUploading", "uploadProgress", "error", "existingMedia"], outputs: ["fileSelected", "fileRemoved"] }, { kind: "component", type: FocalPointPickerComponent, selector: "mcms-focal-point-picker", inputs: ["imageUrl", "focalPoint", "alt", "naturalWidth", "naturalHeight", "imageSizes"], outputs: ["focalPointChange"] }, { kind: "component", type: ImageVariantsDisplay, selector: "mcms-image-variants-display", inputs: ["sizes"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5477
6045
|
}
|
|
5478
6046
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: EntityFormWidget, decorators: [{
|
|
5479
6047
|
type: Component,
|
|
@@ -5492,6 +6060,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
5492
6060
|
BreadcrumbSeparator,
|
|
5493
6061
|
VersionHistoryWidget,
|
|
5494
6062
|
CollectionUploadZoneComponent,
|
|
6063
|
+
FocalPointPickerComponent,
|
|
6064
|
+
ImageVariantsDisplay,
|
|
5495
6065
|
],
|
|
5496
6066
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
5497
6067
|
host: { class: 'block' },
|
|
@@ -5557,7 +6127,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
5557
6127
|
<mcms-collection-upload-zone
|
|
5558
6128
|
[uploadConfig]="collection().upload"
|
|
5559
6129
|
[pendingFile]="pendingFile()"
|
|
5560
|
-
[existingMedia]="mode()
|
|
6130
|
+
[existingMedia]="mode() !== 'create' && !pendingFile() ? formModel() : null"
|
|
5561
6131
|
[disabled]="mode() === 'view'"
|
|
5562
6132
|
[isUploading]="isUploadingFile()"
|
|
5563
6133
|
[uploadProgress]="uploadFileProgress()"
|
|
@@ -5567,6 +6137,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
5567
6137
|
/>
|
|
5568
6138
|
}
|
|
5569
6139
|
|
|
6140
|
+
@if (isUploadCol() && isImageFile() && focalPointImageUrl()) {
|
|
6141
|
+
<div class="mb-6" [class.pointer-events-none]="mode() === 'view'">
|
|
6142
|
+
<p class="mb-2 text-sm font-medium">Focal Point</p>
|
|
6143
|
+
<mcms-focal-point-picker
|
|
6144
|
+
[imageUrl]="focalPointImageUrl()"
|
|
6145
|
+
[focalPoint]="currentFocalPoint()"
|
|
6146
|
+
[naturalWidth]="imageNaturalDimensions().width"
|
|
6147
|
+
[naturalHeight]="imageNaturalDimensions().height"
|
|
6148
|
+
[alt]="focalPointAlt()"
|
|
6149
|
+
[imageSizes]="uploadImageSizes()"
|
|
6150
|
+
(focalPointChange)="onFocalPointChange($event)"
|
|
6151
|
+
/>
|
|
6152
|
+
</div>
|
|
6153
|
+
}
|
|
6154
|
+
|
|
6155
|
+
@if (isUploadCol() && formModelSizes()) {
|
|
6156
|
+
<div class="mb-6">
|
|
6157
|
+
<mcms-image-variants-display [sizes]="formModelSizes()" />
|
|
6158
|
+
</div>
|
|
6159
|
+
}
|
|
6160
|
+
|
|
5570
6161
|
<div class="space-y-6">
|
|
5571
6162
|
@for (field of visibleFields(); track field.name) {
|
|
5572
6163
|
<mcms-field-renderer
|
|
@@ -5945,6 +6536,81 @@ class EntityViewWidget {
|
|
|
5945
6536
|
canDelete = computed(() => {
|
|
5946
6537
|
return this.collectionAccess.canDelete(this.collection().slug);
|
|
5947
6538
|
}, ...(ngDevMode ? [{ debugName: "canDelete" }] : []));
|
|
6539
|
+
/** Whether this collection is an upload collection */
|
|
6540
|
+
isUploadCol = computed(() => isUploadCollection(this.collection()), ...(ngDevMode ? [{ debugName: "isUploadCol" }] : []));
|
|
6541
|
+
/** Whether the entity is an image */
|
|
6542
|
+
isEntityImage = computed(() => {
|
|
6543
|
+
const e = this.entity();
|
|
6544
|
+
if (!e)
|
|
6545
|
+
return false;
|
|
6546
|
+
const mimeType = e['mimeType'];
|
|
6547
|
+
return typeof mimeType === 'string' && mimeType.startsWith('image/');
|
|
6548
|
+
}, ...(ngDevMode ? [{ debugName: "isEntityImage" }] : []));
|
|
6549
|
+
/** Media URL for preview */
|
|
6550
|
+
entityMediaUrl = computed(() => {
|
|
6551
|
+
const e = this.entity();
|
|
6552
|
+
if (!e)
|
|
6553
|
+
return '';
|
|
6554
|
+
if (typeof e['url'] === 'string' && e['url'])
|
|
6555
|
+
return e['url'];
|
|
6556
|
+
if (typeof e['path'] === 'string' && e['path'])
|
|
6557
|
+
return `/api/media/file/${e['path']}`;
|
|
6558
|
+
return '';
|
|
6559
|
+
}, ...(ngDevMode ? [{ debugName: "entityMediaUrl" }] : []));
|
|
6560
|
+
/** Focal point from entity data */
|
|
6561
|
+
entityFocalPoint = computed(() => {
|
|
6562
|
+
const e = this.entity();
|
|
6563
|
+
if (!e)
|
|
6564
|
+
return { x: 0.5, y: 0.5 };
|
|
6565
|
+
const fp = e['focalPoint'];
|
|
6566
|
+
if (fp != null && typeof fp === 'object' && !Array.isArray(fp)) {
|
|
6567
|
+
const obj = fp; // eslint-disable-line @typescript-eslint/consistent-type-assertions
|
|
6568
|
+
const x = obj['x'];
|
|
6569
|
+
const y = obj['y'];
|
|
6570
|
+
if (typeof x === 'number' && typeof y === 'number')
|
|
6571
|
+
return { x, y };
|
|
6572
|
+
}
|
|
6573
|
+
return { x: 0.5, y: 0.5 };
|
|
6574
|
+
}, ...(ngDevMode ? [{ debugName: "entityFocalPoint" }] : []));
|
|
6575
|
+
/** Image dimensions from entity data */
|
|
6576
|
+
entityDimensions = computed(() => {
|
|
6577
|
+
const e = this.entity();
|
|
6578
|
+
return {
|
|
6579
|
+
width: typeof e?.['width'] === 'number' ? e['width'] : 0,
|
|
6580
|
+
height: typeof e?.['height'] === 'number' ? e['height'] : 0,
|
|
6581
|
+
};
|
|
6582
|
+
}, ...(ngDevMode ? [{ debugName: "entityDimensions" }] : []));
|
|
6583
|
+
/** Image sizes from collection upload config */
|
|
6584
|
+
viewImageSizes = computed(() => {
|
|
6585
|
+
return this.collection().upload?.imageSizes ?? [];
|
|
6586
|
+
}, ...(ngDevMode ? [{ debugName: "viewImageSizes" }] : []));
|
|
6587
|
+
/** Generated image sizes from entity data */
|
|
6588
|
+
entitySizes = computed(() => {
|
|
6589
|
+
const e = this.entity();
|
|
6590
|
+
if (!e)
|
|
6591
|
+
return null;
|
|
6592
|
+
const sizes = e['sizes'];
|
|
6593
|
+
if (sizes != null &&
|
|
6594
|
+
typeof sizes === 'object' &&
|
|
6595
|
+
!Array.isArray(sizes) &&
|
|
6596
|
+
Object.keys(sizes).length > 0) {
|
|
6597
|
+
return sizes; // eslint-disable-line @typescript-eslint/consistent-type-assertions
|
|
6598
|
+
}
|
|
6599
|
+
return null;
|
|
6600
|
+
}, ...(ngDevMode ? [{ debugName: "entitySizes" }] : []));
|
|
6601
|
+
/** Media preview data for non-image files */
|
|
6602
|
+
entityMediaPreview = computed(() => {
|
|
6603
|
+
const e = this.entity();
|
|
6604
|
+
if (!e)
|
|
6605
|
+
return null;
|
|
6606
|
+
return {
|
|
6607
|
+
url: typeof e['url'] === 'string' ? e['url'] : undefined,
|
|
6608
|
+
path: typeof e['path'] === 'string' ? e['path'] : undefined,
|
|
6609
|
+
mimeType: typeof e['mimeType'] === 'string' ? e['mimeType'] : undefined,
|
|
6610
|
+
filename: typeof e['filename'] === 'string' ? e['filename'] : undefined,
|
|
6611
|
+
alt: typeof e['alt'] === 'string' ? e['alt'] : undefined,
|
|
6612
|
+
};
|
|
6613
|
+
}, ...(ngDevMode ? [{ debugName: "entityMediaPreview" }] : []));
|
|
5948
6614
|
/** Whether collection has soft delete enabled */
|
|
5949
6615
|
hasSoftDelete = computed(() => !!this.collection().softDelete, ...(ngDevMode ? [{ debugName: "hasSoftDelete" }] : []));
|
|
5950
6616
|
/** Whether the current entity is soft-deleted */
|
|
@@ -6400,6 +7066,28 @@ class EntityViewWidget {
|
|
|
6400
7066
|
{{ loadError() }}
|
|
6401
7067
|
</mcms-alert>
|
|
6402
7068
|
} @else if (entity()) {
|
|
7069
|
+
@if (isUploadCol() && entityMediaUrl()) {
|
|
7070
|
+
<div class="mb-6">
|
|
7071
|
+
@if (isEntityImage()) {
|
|
7072
|
+
<div class="pointer-events-none">
|
|
7073
|
+
<mcms-focal-point-picker
|
|
7074
|
+
[imageUrl]="entityMediaUrl()"
|
|
7075
|
+
[focalPoint]="entityFocalPoint()"
|
|
7076
|
+
[naturalWidth]="entityDimensions().width"
|
|
7077
|
+
[naturalHeight]="entityDimensions().height"
|
|
7078
|
+
[imageSizes]="viewImageSizes()"
|
|
7079
|
+
/>
|
|
7080
|
+
</div>
|
|
7081
|
+
} @else {
|
|
7082
|
+
<mcms-media-preview [media]="entityMediaPreview()" size="xl" />
|
|
7083
|
+
}
|
|
7084
|
+
</div>
|
|
7085
|
+
}
|
|
7086
|
+
@if (isUploadCol() && entitySizes()) {
|
|
7087
|
+
<div class="mb-6">
|
|
7088
|
+
<mcms-image-variants-display [sizes]="entitySizes()" />
|
|
7089
|
+
</div>
|
|
7090
|
+
}
|
|
6403
7091
|
<div class="grid gap-6 md:grid-cols-2">
|
|
6404
7092
|
@for (field of visibleFields(); track field.name) {
|
|
6405
7093
|
<mcms-field-display
|
|
@@ -6448,7 +7136,7 @@ class EntityViewWidget {
|
|
|
6448
7136
|
</div>
|
|
6449
7137
|
}
|
|
6450
7138
|
</div>
|
|
6451
|
-
`, isInline: true, dependencies: [{ kind: "component", type: Card, selector: "mcms-card" }, { kind: "component", type: CardContent, selector: "mcms-card-content" }, { kind: "component", type: CardFooter, selector: "mcms-card-footer" }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: Alert, selector: "mcms-alert", inputs: ["variant", "class"] }, { kind: "component", type: Skeleton, selector: "mcms-skeleton", inputs: ["class"] }, { kind: "component", type: FieldDisplay, selector: "mcms-field-display", inputs: ["value", "type", "label", "format", "emptyText", "badgeConfig", "openInNewTab", "maxItems", "class", "fieldMeta", "numberFormat", "dateFormat"] }, { kind: "component", type: Breadcrumbs, selector: "mcms-breadcrumbs", inputs: ["class"] }, { kind: "component", type: BreadcrumbItem, selector: "mcms-breadcrumb-item", inputs: ["href", "current", "class"] }, { kind: "component", type: BreadcrumbSeparator, selector: "mcms-breadcrumb-separator", inputs: ["class"] }, { kind: "component", type: VersionHistoryWidget, selector: "mcms-version-history", inputs: ["collection", "documentId", "documentLabel"], outputs: ["restored"] }, { kind: "component", type: PublishControlsWidget, selector: "mcms-publish-controls", inputs: ["collection", "documentId", "documentLabel", "initialStatus"], outputs: ["statusChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7139
|
+
`, isInline: true, dependencies: [{ kind: "component", type: Card, selector: "mcms-card" }, { kind: "component", type: CardContent, selector: "mcms-card-content" }, { kind: "component", type: CardFooter, selector: "mcms-card-footer" }, { kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: Alert, selector: "mcms-alert", inputs: ["variant", "class"] }, { kind: "component", type: Skeleton, selector: "mcms-skeleton", inputs: ["class"] }, { kind: "component", type: FieldDisplay, selector: "mcms-field-display", inputs: ["value", "type", "label", "format", "emptyText", "badgeConfig", "openInNewTab", "maxItems", "class", "fieldMeta", "numberFormat", "dateFormat"] }, { kind: "component", type: Breadcrumbs, selector: "mcms-breadcrumbs", inputs: ["class"] }, { kind: "component", type: BreadcrumbItem, selector: "mcms-breadcrumb-item", inputs: ["href", "current", "class"] }, { kind: "component", type: BreadcrumbSeparator, selector: "mcms-breadcrumb-separator", inputs: ["class"] }, { kind: "component", type: VersionHistoryWidget, selector: "mcms-version-history", inputs: ["collection", "documentId", "documentLabel"], outputs: ["restored"] }, { kind: "component", type: PublishControlsWidget, selector: "mcms-publish-controls", inputs: ["collection", "documentId", "documentLabel", "initialStatus"], outputs: ["statusChanged"] }, { kind: "component", type: MediaPreviewComponent, selector: "mcms-media-preview", inputs: ["media", "size", "class", "rounded"] }, { kind: "component", type: FocalPointPickerComponent, selector: "mcms-focal-point-picker", inputs: ["imageUrl", "focalPoint", "alt", "naturalWidth", "naturalHeight", "imageSizes"], outputs: ["focalPointChange"] }, { kind: "component", type: ImageVariantsDisplay, selector: "mcms-image-variants-display", inputs: ["sizes"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6452
7140
|
}
|
|
6453
7141
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: EntityViewWidget, decorators: [{
|
|
6454
7142
|
type: Component,
|
|
@@ -6467,6 +7155,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
6467
7155
|
BreadcrumbSeparator,
|
|
6468
7156
|
VersionHistoryWidget,
|
|
6469
7157
|
PublishControlsWidget,
|
|
7158
|
+
MediaPreviewComponent,
|
|
7159
|
+
FocalPointPickerComponent,
|
|
7160
|
+
ImageVariantsDisplay,
|
|
6470
7161
|
],
|
|
6471
7162
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6472
7163
|
host: { class: 'block' },
|
|
@@ -6570,6 +7261,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
6570
7261
|
{{ loadError() }}
|
|
6571
7262
|
</mcms-alert>
|
|
6572
7263
|
} @else if (entity()) {
|
|
7264
|
+
@if (isUploadCol() && entityMediaUrl()) {
|
|
7265
|
+
<div class="mb-6">
|
|
7266
|
+
@if (isEntityImage()) {
|
|
7267
|
+
<div class="pointer-events-none">
|
|
7268
|
+
<mcms-focal-point-picker
|
|
7269
|
+
[imageUrl]="entityMediaUrl()"
|
|
7270
|
+
[focalPoint]="entityFocalPoint()"
|
|
7271
|
+
[naturalWidth]="entityDimensions().width"
|
|
7272
|
+
[naturalHeight]="entityDimensions().height"
|
|
7273
|
+
[imageSizes]="viewImageSizes()"
|
|
7274
|
+
/>
|
|
7275
|
+
</div>
|
|
7276
|
+
} @else {
|
|
7277
|
+
<mcms-media-preview [media]="entityMediaPreview()" size="xl" />
|
|
7278
|
+
}
|
|
7279
|
+
</div>
|
|
7280
|
+
}
|
|
7281
|
+
@if (isUploadCol() && entitySizes()) {
|
|
7282
|
+
<div class="mb-6">
|
|
7283
|
+
<mcms-image-variants-display [sizes]="entitySizes()" />
|
|
7284
|
+
</div>
|
|
7285
|
+
}
|
|
6573
7286
|
<div class="grid gap-6 md:grid-cols-2">
|
|
6574
7287
|
@for (field of visibleFields(); track field.name) {
|
|
6575
7288
|
<mcms-field-display
|
|
@@ -11452,14 +12165,27 @@ class MediaEditDialog {
|
|
|
11452
12165
|
data = inject(DIALOG_DATA);
|
|
11453
12166
|
api = injectMomentumAPI();
|
|
11454
12167
|
media = this.data.media;
|
|
12168
|
+
imageSizes = this.data.imageSizes ?? [];
|
|
11455
12169
|
filename = signal(this.data.media.filename, ...(ngDevMode ? [{ debugName: "filename" }] : []));
|
|
11456
12170
|
altText = signal(this.data.media.alt ?? '', ...(ngDevMode ? [{ debugName: "altText" }] : []));
|
|
12171
|
+
focalPointValue = signal(this.data.media.focalPoint ?? { x: 0.5, y: 0.5 }, ...(ngDevMode ? [{ debugName: "focalPointValue" }] : []));
|
|
11457
12172
|
isSaving = signal(false, ...(ngDevMode ? [{ debugName: "isSaving" }] : []));
|
|
11458
12173
|
saveError = signal(null, ...(ngDevMode ? [{ debugName: "saveError" }] : []));
|
|
11459
12174
|
formattedSize = formatFileSize(this.data.media.filesize);
|
|
12175
|
+
isImage = computed(() => this.media.mimeType.startsWith('image/'), ...(ngDevMode ? [{ debugName: "isImage" }] : []));
|
|
12176
|
+
imageUrl = computed(() => {
|
|
12177
|
+
return this.media.url ?? `/api/media/file/${this.media.path}`;
|
|
12178
|
+
}, ...(ngDevMode ? [{ debugName: "imageUrl" }] : []));
|
|
11460
12179
|
hasChanges = computed(() => {
|
|
11461
|
-
|
|
12180
|
+
const fpChanged = this.focalPointValue().x !== (this.media.focalPoint?.x ?? 0.5) ||
|
|
12181
|
+
this.focalPointValue().y !== (this.media.focalPoint?.y ?? 0.5);
|
|
12182
|
+
return (this.filename() !== this.media.filename ||
|
|
12183
|
+
this.altText() !== (this.media.alt ?? '') ||
|
|
12184
|
+
fpChanged);
|
|
11462
12185
|
}, ...(ngDevMode ? [{ debugName: "hasChanges" }] : []));
|
|
12186
|
+
onFocalPointChange(fp) {
|
|
12187
|
+
this.focalPointValue.set(fp);
|
|
12188
|
+
}
|
|
11463
12189
|
/**
|
|
11464
12190
|
* Save media metadata changes via API.
|
|
11465
12191
|
*/
|
|
@@ -11467,10 +12193,14 @@ class MediaEditDialog {
|
|
|
11467
12193
|
this.isSaving.set(true);
|
|
11468
12194
|
this.saveError.set(null);
|
|
11469
12195
|
try {
|
|
11470
|
-
const
|
|
12196
|
+
const updateData = {
|
|
11471
12197
|
filename: this.filename(),
|
|
11472
12198
|
alt: this.altText(),
|
|
11473
|
-
}
|
|
12199
|
+
};
|
|
12200
|
+
if (this.isImage()) {
|
|
12201
|
+
updateData['focalPoint'] = this.focalPointValue();
|
|
12202
|
+
}
|
|
12203
|
+
const result = await this.api.collection('media').update(this.media.id, updateData);
|
|
11474
12204
|
if (isMediaEditItem(result)) {
|
|
11475
12205
|
this.dialogRef.close({ updated: true, media: result });
|
|
11476
12206
|
}
|
|
@@ -11527,6 +12257,21 @@ class MediaEditDialog {
|
|
|
11527
12257
|
</div>
|
|
11528
12258
|
</div>
|
|
11529
12259
|
|
|
12260
|
+
@if (isImage()) {
|
|
12261
|
+
<div class="mt-4">
|
|
12262
|
+
<p class="mb-2 text-sm font-medium">Focal Point</p>
|
|
12263
|
+
<mcms-focal-point-picker
|
|
12264
|
+
[imageUrl]="imageUrl()"
|
|
12265
|
+
[focalPoint]="focalPointValue()"
|
|
12266
|
+
[naturalWidth]="media.width ?? 0"
|
|
12267
|
+
[naturalHeight]="media.height ?? 0"
|
|
12268
|
+
[alt]="media.alt ?? media.filename"
|
|
12269
|
+
[imageSizes]="imageSizes"
|
|
12270
|
+
(focalPointChange)="onFocalPointChange($event)"
|
|
12271
|
+
/>
|
|
12272
|
+
</div>
|
|
12273
|
+
}
|
|
12274
|
+
|
|
11530
12275
|
@if (saveError()) {
|
|
11531
12276
|
<mcms-alert variant="destructive" class="mt-4">
|
|
11532
12277
|
{{ saveError() }}
|
|
@@ -11544,7 +12289,7 @@ class MediaEditDialog {
|
|
|
11544
12289
|
</button>
|
|
11545
12290
|
</mcms-dialog-footer>
|
|
11546
12291
|
</mcms-dialog>
|
|
11547
|
-
`, isInline: true, dependencies: [{ kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: Dialog, selector: "mcms-dialog", inputs: ["class"] }, { kind: "component", type: DialogHeader, selector: "mcms-dialog-header" }, { kind: "component", type: DialogTitle, selector: "mcms-dialog-title", inputs: ["id"] }, { kind: "component", type: DialogContent, selector: "mcms-dialog-content" }, { kind: "component", type: DialogFooter, selector: "mcms-dialog-footer" }, { kind: "directive", type: DialogClose, selector: "[mcmsDialogClose]", inputs: ["mcmsDialogClose"] }, { kind: "component", type: Input, selector: "mcms-input", inputs: ["value", "disabled", "errors", "touched", "invalid", "readonly", "required", "type", "id", "name", "placeholder", "autocomplete", "ariaLabel", "describedBy", "min", "max", "step"], outputs: ["valueChange", "blurred"] }, { kind: "component", type: Textarea, selector: "mcms-textarea", inputs: ["value", "disabled", "errors", "touched", "invalid", "readonly", "required", "id", "name", "placeholder", "rows", "ariaLabel", "describedBy"], outputs: ["valueChange", "blurred"] }, { kind: "component", type: Label, selector: "mcms-label", inputs: ["for", "required", "disabled", "class"] }, { kind: "component", type: Spinner, selector: "mcms-spinner", inputs: ["size", "label", "class"] }, { kind: "component", type: Alert, selector: "mcms-alert", inputs: ["variant", "class"] }, { kind: "component", type: MediaPreviewComponent, selector: "mcms-media-preview", inputs: ["media", "size", "class", "rounded"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12292
|
+
`, isInline: true, dependencies: [{ kind: "component", type: Button, selector: "button[mcms-button], a[mcms-button]", inputs: ["variant", "size", "disabled", "loading", "ariaLabel", "class"] }, { kind: "component", type: Dialog, selector: "mcms-dialog", inputs: ["class"] }, { kind: "component", type: DialogHeader, selector: "mcms-dialog-header" }, { kind: "component", type: DialogTitle, selector: "mcms-dialog-title", inputs: ["id"] }, { kind: "component", type: DialogContent, selector: "mcms-dialog-content" }, { kind: "component", type: DialogFooter, selector: "mcms-dialog-footer" }, { kind: "directive", type: DialogClose, selector: "[mcmsDialogClose]", inputs: ["mcmsDialogClose"] }, { kind: "component", type: Input, selector: "mcms-input", inputs: ["value", "disabled", "errors", "touched", "invalid", "readonly", "required", "type", "id", "name", "placeholder", "autocomplete", "ariaLabel", "describedBy", "min", "max", "step"], outputs: ["valueChange", "blurred"] }, { kind: "component", type: Textarea, selector: "mcms-textarea", inputs: ["value", "disabled", "errors", "touched", "invalid", "readonly", "required", "id", "name", "placeholder", "rows", "ariaLabel", "describedBy"], outputs: ["valueChange", "blurred"] }, { kind: "component", type: Label, selector: "mcms-label", inputs: ["for", "required", "disabled", "class"] }, { kind: "component", type: Spinner, selector: "mcms-spinner", inputs: ["size", "label", "class"] }, { kind: "component", type: Alert, selector: "mcms-alert", inputs: ["variant", "class"] }, { kind: "component", type: MediaPreviewComponent, selector: "mcms-media-preview", inputs: ["media", "size", "class", "rounded"] }, { kind: "component", type: FocalPointPickerComponent, selector: "mcms-focal-point-picker", inputs: ["imageUrl", "focalPoint", "alt", "naturalWidth", "naturalHeight", "imageSizes"], outputs: ["focalPointChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11548
12293
|
}
|
|
11549
12294
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MediaEditDialog, decorators: [{
|
|
11550
12295
|
type: Component,
|
|
@@ -11564,6 +12309,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
11564
12309
|
Spinner,
|
|
11565
12310
|
Alert,
|
|
11566
12311
|
MediaPreviewComponent,
|
|
12312
|
+
FocalPointPickerComponent,
|
|
11567
12313
|
],
|
|
11568
12314
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
11569
12315
|
host: { style: 'display: block; width: 100%' },
|
|
@@ -11608,6 +12354,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
11608
12354
|
</div>
|
|
11609
12355
|
</div>
|
|
11610
12356
|
|
|
12357
|
+
@if (isImage()) {
|
|
12358
|
+
<div class="mt-4">
|
|
12359
|
+
<p class="mb-2 text-sm font-medium">Focal Point</p>
|
|
12360
|
+
<mcms-focal-point-picker
|
|
12361
|
+
[imageUrl]="imageUrl()"
|
|
12362
|
+
[focalPoint]="focalPointValue()"
|
|
12363
|
+
[naturalWidth]="media.width ?? 0"
|
|
12364
|
+
[naturalHeight]="media.height ?? 0"
|
|
12365
|
+
[alt]="media.alt ?? media.filename"
|
|
12366
|
+
[imageSizes]="imageSizes"
|
|
12367
|
+
(focalPointChange)="onFocalPointChange($event)"
|
|
12368
|
+
/>
|
|
12369
|
+
</div>
|
|
12370
|
+
}
|
|
12371
|
+
|
|
11611
12372
|
@if (saveError()) {
|
|
11612
12373
|
<mcms-alert variant="destructive" class="mt-4">
|
|
11613
12374
|
{{ saveError() }}
|
|
@@ -11668,6 +12429,7 @@ function getInputElement(event) {
|
|
|
11668
12429
|
*/
|
|
11669
12430
|
class MediaLibraryPage {
|
|
11670
12431
|
document = inject(DOCUMENT);
|
|
12432
|
+
route = inject(ActivatedRoute);
|
|
11671
12433
|
api = injectMomentumAPI();
|
|
11672
12434
|
uploadService = inject(UploadService);
|
|
11673
12435
|
feedback = inject(FeedbackService);
|
|
@@ -11675,6 +12437,12 @@ class MediaLibraryPage {
|
|
|
11675
12437
|
dialog = inject(DialogService);
|
|
11676
12438
|
destroyRef = inject(DestroyRef);
|
|
11677
12439
|
uploadSubscriptions = [];
|
|
12440
|
+
/** Image sizes config from the media collection (for crop previews in edit dialog) */
|
|
12441
|
+
mediaImageSizes = (() => {
|
|
12442
|
+
const collections = getCollectionsFromRouteData(this.route.parent?.snapshot.data);
|
|
12443
|
+
const mediaColl = collections.find((c) => c.slug === 'media');
|
|
12444
|
+
return mediaColl?.upload?.imageSizes ?? [];
|
|
12445
|
+
})();
|
|
11678
12446
|
/** Internal state */
|
|
11679
12447
|
isLoading = signal(true, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
11680
12448
|
mediaItems = signal([], ...(ngDevMode ? [{ debugName: "mediaItems" }] : []));
|
|
@@ -11826,7 +12594,7 @@ class MediaLibraryPage {
|
|
|
11826
12594
|
*/
|
|
11827
12595
|
editMedia(media) {
|
|
11828
12596
|
const dialogRef = this.dialog.open(MediaEditDialog, {
|
|
11829
|
-
data: { media },
|
|
12597
|
+
data: { media, imageSizes: this.mediaImageSizes },
|
|
11830
12598
|
width: '36rem',
|
|
11831
12599
|
});
|
|
11832
12600
|
dialogRef.afterClosed.subscribe((result) => {
|
|
@@ -13454,18 +14222,18 @@ function provideMomentumFieldRenderers() {
|
|
|
13454
14222
|
registry.register('checkbox', () => Promise.resolve().then(function () { return checkboxField_component; }).then((m) => m.CheckboxFieldRenderer));
|
|
13455
14223
|
registry.register('date', () => Promise.resolve().then(function () { return dateField_component; }).then((m) => m.DateFieldRenderer));
|
|
13456
14224
|
registry.register('upload', () => Promise.resolve().then(function () { return uploadField_component; }).then((m) => m.UploadFieldRenderer));
|
|
13457
|
-
registry.register('richText', () => import('./momentumcms-admin-rich-text-field.component-
|
|
14225
|
+
registry.register('richText', () => import('./momentumcms-admin-rich-text-field.component-BIUu6NXa.mjs').then((m) => m.RichTextFieldRenderer));
|
|
13458
14226
|
// Layout field renderers (support nested field rendering)
|
|
13459
|
-
registry.register('group', () => import('./momentumcms-admin-group-field.component-
|
|
13460
|
-
registry.register('array', () => import('./momentumcms-admin-array-field.component-
|
|
13461
|
-
registry.register('blocks', () => import('./momentumcms-admin-blocks-field.component-
|
|
14227
|
+
registry.register('group', () => import('./momentumcms-admin-group-field.component-Bofhumd5.mjs').then((m) => m.GroupFieldRenderer));
|
|
14228
|
+
registry.register('array', () => import('./momentumcms-admin-array-field.component-ChjP5zK9.mjs').then((m) => m.ArrayFieldRenderer));
|
|
14229
|
+
registry.register('blocks', () => import('./momentumcms-admin-blocks-field.component-Dl0FxGnf.mjs').then((m) => m.BlocksFieldRenderer));
|
|
13462
14230
|
// Visual block editor variant (blocks field with admin.editor === 'visual')
|
|
13463
14231
|
registry.register('blocks-visual', () => Promise.resolve().then(function () { return visualBlockEditor_component; }).then((m) => m.VisualBlockEditorComponent));
|
|
13464
|
-
registry.register('relationship', () => import('./momentumcms-admin-relationship-field.component-
|
|
14232
|
+
registry.register('relationship', () => import('./momentumcms-admin-relationship-field.component-DSZhc5MP.mjs').then((m) => m.RelationshipFieldRenderer));
|
|
13465
14233
|
// Layout-only renderers (tabs, collapsible, row)
|
|
13466
|
-
registry.register('tabs', () => import('./momentumcms-admin-tabs-field.component-
|
|
13467
|
-
registry.register('collapsible', () => import('./momentumcms-admin-collapsible-field.component-
|
|
13468
|
-
registry.register('row', () => import('./momentumcms-admin-row-field.component-
|
|
14234
|
+
registry.register('tabs', () => import('./momentumcms-admin-tabs-field.component-BsnCWC5J.mjs').then((m) => m.TabsFieldRenderer));
|
|
14235
|
+
registry.register('collapsible', () => import('./momentumcms-admin-collapsible-field.component-CSLOT0Dp.mjs').then((m) => m.CollapsibleFieldRenderer));
|
|
14236
|
+
registry.register('row', () => import('./momentumcms-admin-row-field.component-DBzqzooT.mjs').then((m) => m.RowFieldRenderer));
|
|
13469
14237
|
};
|
|
13470
14238
|
},
|
|
13471
14239
|
},
|
|
@@ -15245,4 +16013,4 @@ var uploadField_component = /*#__PURE__*/Object.freeze({
|
|
|
15245
16013
|
*/
|
|
15246
16014
|
|
|
15247
16015
|
export { adminGuard as $, AdminShellComponent as A, BlockEditDialog as B, CheckboxFieldRenderer as C, DashboardPage as D, EntityFormWidget as E, FieldRenderer as F, MediaLibraryPage as G, MediaPickerDialog as H, MediaPreviewComponent as I, MomentumApiService as J, MomentumAuthService as K, LivePreviewComponent as L, MOMENTUM_API as M, NumberFieldRenderer as N, ResetPasswordPage as O, PublishControlsWidget as P, SKIP_AUTO_TOAST as Q, ResetPasswordFormComponent as R, SHEET_QUERY_PARAMS as S, SelectFieldRenderer as T, SetupPage as U, TextFieldRenderer as V, UploadFieldRenderer as W, UploadService as X, VersionHistoryWidget as Y, VersionService as Z, VisualBlockEditorComponent as _, getFieldNodeState as a, authGuard as a0, collectionAccessGuard as a1, crudToastInterceptor as a2, guestGuard as a3, injectHasAnyRole as a4, injectHasRole as a5, injectIsAdmin as a6, injectIsAuthenticated as a7, injectMomentumAPI as a8, injectTypedMomentumAPI as a9, injectUser as aa, injectUserRole as ab, injectVersionService as ac, momentumAdminRoutes as ad, provideFieldRenderer as ae, provideMomentumAPI as af, provideMomentumFieldRenderers as ag, setupGuard as ah, unsavedChangesGuard as ai, getSubNode as b, getFieldDefaultValue as c, EntitySheetService as d, getTitleField as e, AdminSidebarWidget as f, getGlobalsFromRouteData as g, BlockInserterComponent as h, isRecord as i, BlockWrapperComponent as j, CollectionAccessService as k, CollectionCardWidget as l, CollectionEditPage as m, normalizeBlockDefaults as n, CollectionListPage as o, CollectionViewPage as p, DateFieldRenderer as q, EntityListWidget as r, EntityViewWidget as s, FeedbackService as t, FieldRendererRegistry as u, ForgotPasswordFormComponent as v, ForgotPasswordPage as w, LoginPage as x, MOMENTUM_API_CONTEXT as y, McmsThemeService as z };
|
|
15248
|
-
//# sourceMappingURL=momentumcms-admin-momentumcms-admin-
|
|
16016
|
+
//# sourceMappingURL=momentumcms-admin-momentumcms-admin-D8WvqCxe.mjs.map
|