@origostudio/origo-design 0.0.52 → 0.0.53
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/CHANGELOG.md +3 -6
- package/dist/packages/angular-renderer/CHANGELOG.md +10 -0
- package/dist/packages/angular-renderer/package.json +4 -3
- package/dist/packages/angular-renderer/src/components/primitives/date-range-picker/date-range-picker.component.d.ts +1 -1
- package/dist/packages/angular-renderer/src/components/primitives/date-range-picker/date-range-picker.component.js.map +1 -1
- package/dist/packages/angular-renderer/src/components/primitives/divider/divider.component.js +1 -1
- package/dist/packages/angular-renderer/src/components/primitives/divider/divider.component.js.map +1 -1
- package/dist/packages/angular-renderer/src/components/primitives/dropzone/dropzone.component.d.ts +70 -0
- package/dist/packages/angular-renderer/src/components/primitives/dropzone/dropzone.component.js +263 -0
- package/dist/packages/angular-renderer/src/components/primitives/dropzone/dropzone.component.js.map +1 -0
- package/dist/packages/angular-renderer/src/components/primitives/file-input/file-input.component.d.ts +64 -0
- package/dist/packages/angular-renderer/src/components/primitives/file-input/file-input.component.js +192 -0
- package/dist/packages/angular-renderer/src/components/primitives/file-input/file-input.component.js.map +1 -0
- package/dist/packages/angular-renderer/src/components/primitives/grid/grid.component.js +2 -2
- package/dist/packages/angular-renderer/src/components/primitives/grid/grid.component.js.map +1 -1
- package/dist/packages/angular-renderer/src/components/primitives/rich-text-editor/rich-text-editor.component.d.ts +44 -0
- package/dist/packages/angular-renderer/src/components/primitives/rich-text-editor/rich-text-editor.component.js +143 -0
- package/dist/packages/angular-renderer/src/components/primitives/rich-text-editor/rich-text-editor.component.js.map +1 -0
- package/dist/packages/angular-renderer/src/components/primitives/stack/stack.component.js +3 -3
- package/dist/packages/angular-renderer/src/components/primitives/stack/stack.component.js.map +1 -1
- package/dist/packages/angular-renderer/src/index.d.ts +3 -0
- package/dist/packages/angular-renderer/src/index.js +3 -0
- package/dist/packages/angular-renderer/src/index.js.map +1 -1
- package/dist/packages/angular-renderer/src/lib/primitives.provider.js +6 -0
- package/dist/packages/angular-renderer/src/lib/primitives.provider.js.map +1 -1
- package/dist/packages/cli/CHANGELOG.md +4 -0
- package/dist/packages/cli/package.json +2 -2
- package/dist/packages/cli/package.json.bak +2 -2
- package/dist/packages/core/CHANGELOG.md +4 -0
- package/dist/packages/core/package.json +1 -1
- package/dist/packages/core/package.json.bak +1 -1
- package/dist/packages/design-tokens/CHANGELOG.md +4 -0
- package/dist/packages/design-tokens/package.json +1 -1
- package/dist/packages/playground/browser/{chunk-DPJ2_PnQ.js → chunk-BD3aW3fo.js} +1 -1
- package/dist/packages/playground/browser/{chunk-ClEEX1ib.js → chunk-BGrPMh-Q.js} +1 -1
- package/dist/packages/playground/browser/{chunk-IRlAEjxV.js → chunk-Bb34YjCX.js} +1 -1
- package/dist/packages/playground/browser/{chunk-BDJ6g_eZ.js → chunk-CYa5xYdO.js} +1 -1
- package/dist/packages/playground/browser/{chunk-kBISgbOR.js → chunk-CZYE6Vo5.js} +1 -1
- package/dist/packages/playground/browser/{chunk-BRY4TA8M.js → chunk-CoyK3eaz.js} +1 -1
- package/dist/packages/playground/browser/{chunk-D8i35nMZ.js → chunk-Cxq4lcyl.js} +1 -1
- package/dist/packages/playground/browser/{chunk-kdIujLCE.js → chunk-D-gQXFhO.js} +1 -1
- package/dist/packages/playground/browser/{chunk-lkXWJECn.js → chunk-DFUtHoSa.js} +1 -1
- package/dist/packages/playground/browser/{chunk-BdpxuY7o.js → chunk-DGFBCrz0.js} +1 -1
- package/dist/packages/playground/browser/{chunk-C18SRlXx.js → chunk-DjQs2rZT.js} +1 -1
- package/dist/packages/playground/browser/{chunk-FPNisYyW.js → chunk-DoDHfFRC.js} +1 -1
- package/dist/packages/playground/browser/{chunk-MmjtX4PA.js → chunk-K_6Vunnz.js} +1 -1
- package/dist/packages/playground/browser/{chunk-DpG_PyU_.js → chunk-OWSRr_rS.js} +1 -1
- package/dist/packages/playground/browser/{chunk-BKu6N-zK.js → chunk-kcBcpdDQ.js} +1 -1
- package/dist/packages/playground/browser/index.html +1 -1
- package/dist/packages/playground/browser/main-ZTUYGW6H.js +748 -0
- package/package.json +1 -1
- package/packages/angular-renderer/CHANGELOG.md +10 -0
- package/packages/angular-renderer/package.json +4 -3
- package/packages/angular-renderer/src/components/primitives/date-range-picker/date-range-picker.component.ts +1 -1
- package/packages/angular-renderer/src/components/primitives/divider/divider.component.ts +2 -2
- package/packages/angular-renderer/src/components/primitives/dropzone/dropzone.component.html +24 -0
- package/packages/angular-renderer/src/components/primitives/dropzone/dropzone.component.scss +32 -0
- package/packages/angular-renderer/src/components/primitives/dropzone/dropzone.component.ts +293 -0
- package/packages/angular-renderer/src/components/primitives/file-input/file-input.component.html +13 -0
- package/packages/angular-renderer/src/components/primitives/file-input/file-input.component.scss +32 -0
- package/packages/angular-renderer/src/components/primitives/file-input/file-input.component.ts +216 -0
- package/packages/angular-renderer/src/components/primitives/grid/grid.component.ts +6 -5
- package/packages/angular-renderer/src/components/primitives/rich-text-editor/rich-text-editor.component.html +23 -0
- package/packages/angular-renderer/src/components/primitives/rich-text-editor/rich-text-editor.component.scss +33 -0
- package/packages/angular-renderer/src/components/primitives/rich-text-editor/rich-text-editor.component.ts +165 -0
- package/packages/angular-renderer/src/components/primitives/stack/stack.component.ts +6 -6
- package/packages/angular-renderer/src/index.ts +3 -0
- package/packages/angular-renderer/src/lib/primitives.provider.ts +6 -0
- package/packages/cli/CHANGELOG.md +4 -0
- package/packages/cli/package.json +2 -2
- package/packages/core/CHANGELOG.md +4 -0
- package/packages/core/package.json +1 -1
- package/packages/design-tokens/CHANGELOG.md +4 -0
- package/packages/design-tokens/package.json +1 -1
- package/packages/devtools/CHANGELOG.md +4 -0
- package/packages/devtools/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/devtools/package.json +3 -3
- package/packages/playground/CHANGELOG.md +4 -0
- package/packages/playground/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/playground/package.json +3 -3
- package/dist/packages/playground/browser/main-CD73IVQX.js +0 -744
package/package.json
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 0.0.53 (2026-10-09)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- implement file-input, dropzone, and rich-text-editor angular primitives ([69cc864](https://github.com/OrigoStudio/origo-design/commit/69cc864))
|
|
6
|
+
|
|
7
|
+
### ❤️ Thank You
|
|
8
|
+
|
|
9
|
+
- Hardik Patel @hardikpatelse
|
|
10
|
+
|
|
1
11
|
## 0.0.52 (2026-10-09)
|
|
2
12
|
|
|
3
13
|
### 🚀 Features
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@origostudio/angular-renderer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.53",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@origostudio/core": "0.0.
|
|
10
|
-
"@origostudio/design-tokens": "0.0.
|
|
9
|
+
"@origostudio/core": "0.0.53",
|
|
10
|
+
"@origostudio/design-tokens": "0.0.53",
|
|
11
11
|
"tslib": "^2.3.0",
|
|
12
12
|
"@analogjs/vite-plugin-angular": "^2.7.1",
|
|
13
13
|
"zone.js": "^0.16.3",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"peerDependencies": {
|
|
17
17
|
"@angular/common": ">=18.0.0",
|
|
18
18
|
"@angular/core": ">=18.0.0",
|
|
19
|
+
"@angular/forms": ">=18.0.0",
|
|
19
20
|
"@angular/platform-browser": ">=18.0.0"
|
|
20
21
|
},
|
|
21
22
|
"repository": {
|
|
@@ -61,7 +61,7 @@ export class DateRangePickerComponent implements OrigoAdapter<DateRangePickerPro
|
|
|
61
61
|
contract = input.required<InteractionContract<DateRangePickerProps>>();
|
|
62
62
|
|
|
63
63
|
// DateRangePicker doesn't use a single value model in the same way, but keeping it for adapter compat if needed
|
|
64
|
-
value = model<
|
|
64
|
+
value = model<unknown>(null);
|
|
65
65
|
|
|
66
66
|
startDate = signal<string | null>(null);
|
|
67
67
|
endDate = signal<string | null>(null);
|
|
@@ -37,7 +37,7 @@ const contractSchema: Record<
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
const finalSchema = { ...contractSchema };
|
|
40
|
-
delete (finalSchema as
|
|
40
|
+
delete (finalSchema as Record<string, unknown>)['thickness'];
|
|
41
41
|
|
|
42
42
|
@Component({
|
|
43
43
|
selector: 'origo-divider',
|
|
@@ -64,7 +64,7 @@ export class DividerComponent implements OrigoAdapter<DividerProps> {
|
|
|
64
64
|
protected props = computed(() => {
|
|
65
65
|
const p = coerceContractProps<DividerProps>(this.contract().props, finalSchema);
|
|
66
66
|
const original = this.contract().props || {};
|
|
67
|
-
if ('thickness' in original) p.thickness = original.thickness as
|
|
67
|
+
if ('thickness' in original) p.thickness = original.thickness as string | number;
|
|
68
68
|
return p;
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<div
|
|
2
|
+
class="origo-dropzone__wrapper"
|
|
3
|
+
[class.origo-dropzone--dragging]="isDragging()"
|
|
4
|
+
[class.origo-dropzone--disabled]="disabled"
|
|
5
|
+
(dragover)="onDragOver($event)"
|
|
6
|
+
(dragleave)="onDragLeave($event)"
|
|
7
|
+
(drop)="onDrop($event)"
|
|
8
|
+
(click)="inputEl.click()"
|
|
9
|
+
>
|
|
10
|
+
<input
|
|
11
|
+
#inputEl
|
|
12
|
+
type="file"
|
|
13
|
+
class="origo-dropzone__input"
|
|
14
|
+
[multiple]="computedMultiple()"
|
|
15
|
+
[accept]="computedAccept()"
|
|
16
|
+
[disabled]="disabled"
|
|
17
|
+
(change)="onFileChange($event)"
|
|
18
|
+
(focus)="onFocus($event)"
|
|
19
|
+
(blur)="onBlur($event)"
|
|
20
|
+
/>
|
|
21
|
+
<div class="origo-dropzone__content">
|
|
22
|
+
<ng-container #vc></ng-container>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.origo-dropzone__wrapper {
|
|
6
|
+
position: relative;
|
|
7
|
+
border: 2px dashed var(--origo-color-border-default);
|
|
8
|
+
border-radius: var(--origo-shape-border-radius-medium);
|
|
9
|
+
padding-block: var(--origo-spacing-large);
|
|
10
|
+
padding-inline: var(--origo-spacing-large);
|
|
11
|
+
text-align: center;
|
|
12
|
+
transition:
|
|
13
|
+
background-color var(--origo-motion-duration-short, 150ms)
|
|
14
|
+
var(--origo-motion-easing-standard, ease),
|
|
15
|
+
border-color var(--origo-motion-duration-short, 150ms) var(--origo-motion-easing-standard, ease);
|
|
16
|
+
background-color: var(--origo-color-surface-default);
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.origo-dropzone__wrapper.origo-dropzone--dragging {
|
|
21
|
+
background-color: var(--origo-color-surface-hover);
|
|
22
|
+
border-color: var(--origo-color-border-focus);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.origo-dropzone__input {
|
|
26
|
+
width: 0.1px;
|
|
27
|
+
height: 0.1px;
|
|
28
|
+
opacity: 0;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
position: absolute;
|
|
31
|
+
z-index: -1;
|
|
32
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
input,
|
|
4
|
+
ChangeDetectionStrategy,
|
|
5
|
+
ViewEncapsulation,
|
|
6
|
+
forwardRef,
|
|
7
|
+
viewChild,
|
|
8
|
+
ViewContainerRef,
|
|
9
|
+
output,
|
|
10
|
+
computed,
|
|
11
|
+
signal,
|
|
12
|
+
inject,
|
|
13
|
+
ElementRef,
|
|
14
|
+
} from '@angular/core';
|
|
15
|
+
import { InteractionContract } from '@origostudio/core';
|
|
16
|
+
import { OrigoAdapter, coerceContractProps } from '../../../adapters/web/adapter';
|
|
17
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
18
|
+
import { CommonModule } from '@angular/common';
|
|
19
|
+
import { HttpClient, HttpEventType } from '@angular/common/http';
|
|
20
|
+
|
|
21
|
+
export interface DropzoneProps {
|
|
22
|
+
permissions?: Record<string, string>;
|
|
23
|
+
rules?: Record<string, unknown>;
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
multiple?: boolean;
|
|
26
|
+
accept?: string;
|
|
27
|
+
maxFileSize?: number;
|
|
28
|
+
uploadUrl?: string;
|
|
29
|
+
'aria-label'?: string;
|
|
30
|
+
'aria-describedby'?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Component({
|
|
34
|
+
selector: 'origo-dropzone',
|
|
35
|
+
standalone: true,
|
|
36
|
+
imports: [CommonModule],
|
|
37
|
+
templateUrl: './dropzone.component.html',
|
|
38
|
+
styleUrls: ['./dropzone.component.scss'],
|
|
39
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
40
|
+
encapsulation: ViewEncapsulation.ShadowDom,
|
|
41
|
+
providers: [
|
|
42
|
+
{
|
|
43
|
+
provide: NG_VALUE_ACCESSOR,
|
|
44
|
+
useExisting: forwardRef(() => DropzoneComponent),
|
|
45
|
+
multi: true,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
host: {
|
|
49
|
+
'[class.origo-dropzone]': 'true',
|
|
50
|
+
'[attr.data-testid]': 'contract().id',
|
|
51
|
+
'[attr.aria-label]': 'computedAriaLabel()',
|
|
52
|
+
'[attr.aria-describedby]': 'computedAriaDescribedBy()',
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
export class DropzoneComponent implements OrigoAdapter<DropzoneProps>, ControlValueAccessor {
|
|
56
|
+
static readonly contractSchema = {
|
|
57
|
+
permissions: 'object',
|
|
58
|
+
rules: 'object',
|
|
59
|
+
metadata: 'object',
|
|
60
|
+
multiple: 'boolean',
|
|
61
|
+
accept: 'string',
|
|
62
|
+
maxFileSize: 'number',
|
|
63
|
+
uploadUrl: 'string',
|
|
64
|
+
'aria-label': 'string',
|
|
65
|
+
'aria-describedby': 'string',
|
|
66
|
+
} as const;
|
|
67
|
+
static readonly strictContract = false;
|
|
68
|
+
|
|
69
|
+
contract = input.required<InteractionContract<DropzoneProps>>();
|
|
70
|
+
vc = viewChild.required('vc', { read: ViewContainerRef });
|
|
71
|
+
inputRef = viewChild<ElementRef<HTMLInputElement>>('inputEl');
|
|
72
|
+
|
|
73
|
+
fileDrop = output<File[]>();
|
|
74
|
+
fileSelect = output<File[]>();
|
|
75
|
+
fileRemove = output<File>();
|
|
76
|
+
fileError = output<string>();
|
|
77
|
+
clear = output<void>();
|
|
78
|
+
focus = output<FocusEvent>();
|
|
79
|
+
blur = output<FocusEvent>();
|
|
80
|
+
|
|
81
|
+
uploadStart = output<void>();
|
|
82
|
+
uploadProgress = output<number>();
|
|
83
|
+
uploadSuccess = output<unknown>();
|
|
84
|
+
uploadError = output<unknown>();
|
|
85
|
+
|
|
86
|
+
private http = inject(HttpClient, { optional: true });
|
|
87
|
+
|
|
88
|
+
protected props = computed(() => {
|
|
89
|
+
return coerceContractProps<DropzoneProps>(
|
|
90
|
+
this.contract().props,
|
|
91
|
+
DropzoneComponent.contractSchema
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
computedMultiple = computed(() => !!this.props()?.multiple);
|
|
96
|
+
computedAccept = computed(() => this.props()?.accept ?? '*/*');
|
|
97
|
+
computedMaxFileSize = computed(() => this.props()?.maxFileSize);
|
|
98
|
+
computedUploadUrl = computed(() => this.props()?.uploadUrl);
|
|
99
|
+
computedAriaLabel = computed(() => this.props()?.['aria-label'] || null);
|
|
100
|
+
computedAriaDescribedBy = computed(() => this.props()?.['aria-describedby'] || null);
|
|
101
|
+
|
|
102
|
+
disabled = false;
|
|
103
|
+
files = signal<File[]>([]);
|
|
104
|
+
isDragging = signal<boolean>(false);
|
|
105
|
+
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
107
|
+
private onChange: (value: File[]) => void = () => {};
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
109
|
+
private onTouched: () => void = () => {};
|
|
110
|
+
|
|
111
|
+
writeValue(obj: unknown): void {
|
|
112
|
+
if (!obj) {
|
|
113
|
+
this.files.set([]);
|
|
114
|
+
const inputEl = this.inputRef()?.nativeElement;
|
|
115
|
+
if (inputEl) inputEl.value = '';
|
|
116
|
+
} else if (Array.isArray(obj)) {
|
|
117
|
+
this.files.set(obj);
|
|
118
|
+
} else {
|
|
119
|
+
this.files.set([obj as File]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
registerOnChange(fn: (value: File[]) => void): void {
|
|
124
|
+
this.onChange = fn;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
registerOnTouched(fn: () => void): void {
|
|
128
|
+
this.onTouched = fn;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setDisabledState?(isDisabled: boolean): void {
|
|
132
|
+
this.disabled = isDisabled;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
onDragOver(event: DragEvent) {
|
|
136
|
+
if (this.disabled) return;
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
this.isDragging.set(true);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
onDragLeave(event: DragEvent) {
|
|
142
|
+
if (this.disabled) return;
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
|
|
145
|
+
// Ignore dragleave if it's just moving into a child element
|
|
146
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
147
|
+
if (
|
|
148
|
+
event.clientX > rect.left &&
|
|
149
|
+
event.clientX < rect.right &&
|
|
150
|
+
event.clientY > rect.top &&
|
|
151
|
+
event.clientY < rect.bottom
|
|
152
|
+
) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.isDragging.set(false);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
onDrop(event: DragEvent) {
|
|
159
|
+
if (this.disabled) return;
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
this.isDragging.set(false);
|
|
162
|
+
this.onTouched();
|
|
163
|
+
|
|
164
|
+
if (event.dataTransfer?.files) {
|
|
165
|
+
const droppedFiles = Array.from(event.dataTransfer.files);
|
|
166
|
+
if (!droppedFiles.length) return;
|
|
167
|
+
this.processFiles(droppedFiles, 'drop');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
onFileChange(event: Event) {
|
|
172
|
+
const inputEl = event.target as HTMLInputElement;
|
|
173
|
+
if (inputEl.files) {
|
|
174
|
+
const selectedFiles = Array.from(inputEl.files);
|
|
175
|
+
if (!selectedFiles.length) return;
|
|
176
|
+
this.processFiles(selectedFiles, 'select');
|
|
177
|
+
inputEl.value = '';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private processFiles(incomingFiles: File[], source: 'drop' | 'select') {
|
|
182
|
+
const accept = this.computedAccept();
|
|
183
|
+
let validFiles = incomingFiles;
|
|
184
|
+
|
|
185
|
+
if (accept !== '*/*') {
|
|
186
|
+
const acceptedTypes = accept.split(',').map((a: string) => a.trim().toLowerCase());
|
|
187
|
+
validFiles = incomingFiles.filter(file => {
|
|
188
|
+
const fileType = file.type.toLowerCase();
|
|
189
|
+
const fileExt = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
|
|
190
|
+
return acceptedTypes.some((type: string) => {
|
|
191
|
+
if (type.startsWith('.')) return fileExt === type;
|
|
192
|
+
if (type.endsWith('/*')) return fileType.startsWith(type.substring(0, type.length - 1));
|
|
193
|
+
return fileType === type;
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
if (validFiles.length < incomingFiles.length) {
|
|
197
|
+
this.fileError.emit(
|
|
198
|
+
`Some files were rejected because they do not match the accepted types: ${accept}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!validFiles.length) return;
|
|
204
|
+
|
|
205
|
+
const maxFileSize = this.computedMaxFileSize();
|
|
206
|
+
if (typeof maxFileSize === 'number' && maxFileSize >= 0) {
|
|
207
|
+
const invalidFiles = validFiles.filter(file => file.size > maxFileSize);
|
|
208
|
+
if (invalidFiles.length > 0) {
|
|
209
|
+
this.fileError.emit(`File size exceeds maximum allowed size of ${maxFileSize} bytes`);
|
|
210
|
+
validFiles = validFiles.filter(file => file.size <= maxFileSize);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!validFiles.length) return;
|
|
215
|
+
|
|
216
|
+
const currentFiles = this.computedMultiple()
|
|
217
|
+
? [...this.files(), ...validFiles]
|
|
218
|
+
: [...validFiles];
|
|
219
|
+
this.files.set(currentFiles);
|
|
220
|
+
this.onChange(currentFiles);
|
|
221
|
+
|
|
222
|
+
if (source === 'drop') {
|
|
223
|
+
this.fileDrop.emit(validFiles);
|
|
224
|
+
} else {
|
|
225
|
+
this.fileSelect.emit(validFiles);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const uploadUrl = this.computedUploadUrl();
|
|
229
|
+
if (uploadUrl && validFiles.length > 0) {
|
|
230
|
+
this.uploadFiles(validFiles, uploadUrl);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
onFocus(event: FocusEvent) {
|
|
235
|
+
this.focus.emit(event);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
onBlur(event: FocusEvent) {
|
|
239
|
+
this.onTouched();
|
|
240
|
+
this.blur.emit(event);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public removeFile(file: File) {
|
|
244
|
+
const updated = this.files().filter(f => f !== file);
|
|
245
|
+
this.files.set(updated);
|
|
246
|
+
this.onChange(updated);
|
|
247
|
+
this.fileRemove.emit(file);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public clearFiles() {
|
|
251
|
+
this.files.set([]);
|
|
252
|
+
this.onChange([]);
|
|
253
|
+
this.clear.emit();
|
|
254
|
+
const inputEl = this.inputRef()?.nativeElement;
|
|
255
|
+
if (inputEl) inputEl.value = '';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private uploadFiles(filesToUpload: File[], url: string) {
|
|
259
|
+
this.uploadStart.emit();
|
|
260
|
+
|
|
261
|
+
if (!this.http) {
|
|
262
|
+
this.uploadError.emit('HttpClient is not provided.');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const formData = new FormData();
|
|
267
|
+
filesToUpload.forEach(file => formData.append('file', file));
|
|
268
|
+
|
|
269
|
+
this.http
|
|
270
|
+
.post(url, formData, {
|
|
271
|
+
reportProgress: true,
|
|
272
|
+
observe: 'events',
|
|
273
|
+
responseType: 'json',
|
|
274
|
+
})
|
|
275
|
+
.subscribe({
|
|
276
|
+
next: event => {
|
|
277
|
+
if (event.type === HttpEventType.UploadProgress) {
|
|
278
|
+
if (event.total && event.total > 0) {
|
|
279
|
+
const percentComplete = (event.loaded / event.total) * 100;
|
|
280
|
+
this.uploadProgress.emit(percentComplete);
|
|
281
|
+
} else {
|
|
282
|
+
this.uploadProgress.emit(100);
|
|
283
|
+
}
|
|
284
|
+
} else if (event.type === HttpEventType.Response) {
|
|
285
|
+
this.uploadSuccess.emit(event.body);
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
error: err => {
|
|
289
|
+
this.uploadError.emit(err.message || 'Upload failed');
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
package/packages/angular-renderer/src/components/primitives/file-input/file-input.component.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="origo-file-input__wrapper">
|
|
2
|
+
<input
|
|
3
|
+
#inputEl
|
|
4
|
+
type="file"
|
|
5
|
+
[multiple]="computedMultiple()"
|
|
6
|
+
[accept]="computedAccept()"
|
|
7
|
+
[disabled]="disabled"
|
|
8
|
+
(change)="onFileChange($event)"
|
|
9
|
+
(focus)="onFocus($event)"
|
|
10
|
+
(blur)="onBlur($event)"
|
|
11
|
+
/>
|
|
12
|
+
<ng-container #vc></ng-container>
|
|
13
|
+
</div>
|
package/packages/angular-renderer/src/components/primitives/file-input/file-input.component.scss
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.origo-file-input__wrapper {
|
|
6
|
+
position: relative;
|
|
7
|
+
border: 1px solid var(--origo-color-border-default);
|
|
8
|
+
border-radius: var(--origo-shape-border-radius-medium);
|
|
9
|
+
padding-block: var(--origo-spacing-small);
|
|
10
|
+
padding-inline: var(--origo-spacing-small);
|
|
11
|
+
background-color: var(--origo-color-surface-default);
|
|
12
|
+
transition:
|
|
13
|
+
border-color 0.15s ease-in-out,
|
|
14
|
+
box-shadow 0.15s ease-in-out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.origo-file-input__wrapper:focus-within {
|
|
18
|
+
border-color: var(--origo-color-border-focus);
|
|
19
|
+
box-shadow: 0 0 0 2px var(--origo-color-surface-hover);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
input[type='file'] {
|
|
23
|
+
width: 100%;
|
|
24
|
+
font-family: inherit;
|
|
25
|
+
font-size: var(--origo-typography-body-size);
|
|
26
|
+
color: var(--origo-color-text-default);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
input[type='file']:disabled {
|
|
30
|
+
opacity: 0.5;
|
|
31
|
+
cursor: not-allowed;
|
|
32
|
+
}
|
package/packages/angular-renderer/src/components/primitives/file-input/file-input.component.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
input,
|
|
4
|
+
ChangeDetectionStrategy,
|
|
5
|
+
ViewEncapsulation,
|
|
6
|
+
forwardRef,
|
|
7
|
+
viewChild,
|
|
8
|
+
ViewContainerRef,
|
|
9
|
+
output,
|
|
10
|
+
computed,
|
|
11
|
+
inject,
|
|
12
|
+
ElementRef,
|
|
13
|
+
} from '@angular/core';
|
|
14
|
+
import { InteractionContract } from '@origostudio/core';
|
|
15
|
+
import { OrigoAdapter, coerceContractProps } from '../../../adapters/web/adapter';
|
|
16
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
17
|
+
import { HttpClient, HttpEventType } from '@angular/common/http';
|
|
18
|
+
|
|
19
|
+
export interface FileInputProps {
|
|
20
|
+
permissions?: Record<string, string>;
|
|
21
|
+
rules?: Record<string, unknown>;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
multiple?: boolean;
|
|
24
|
+
accept?: string;
|
|
25
|
+
maxFileSize?: number;
|
|
26
|
+
uploadUrl?: string;
|
|
27
|
+
'aria-label'?: string;
|
|
28
|
+
'aria-describedby'?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Component({
|
|
32
|
+
selector: 'origo-file-input',
|
|
33
|
+
standalone: true,
|
|
34
|
+
templateUrl: './file-input.component.html',
|
|
35
|
+
styleUrls: ['./file-input.component.scss'],
|
|
36
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
37
|
+
encapsulation: ViewEncapsulation.ShadowDom,
|
|
38
|
+
providers: [
|
|
39
|
+
{
|
|
40
|
+
provide: NG_VALUE_ACCESSOR,
|
|
41
|
+
useExisting: forwardRef(() => FileInputComponent),
|
|
42
|
+
multi: true,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
host: {
|
|
46
|
+
'[class.origo-file-input]': 'true',
|
|
47
|
+
'[attr.data-testid]': 'contract().id',
|
|
48
|
+
'[attr.aria-label]': 'computedAriaLabel()',
|
|
49
|
+
'[attr.aria-describedby]': 'computedAriaDescribedBy()',
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
export class FileInputComponent implements OrigoAdapter<FileInputProps>, ControlValueAccessor {
|
|
53
|
+
static readonly contractSchema = {
|
|
54
|
+
permissions: 'object',
|
|
55
|
+
rules: 'object',
|
|
56
|
+
metadata: 'object',
|
|
57
|
+
multiple: 'boolean',
|
|
58
|
+
accept: 'string',
|
|
59
|
+
maxFileSize: 'number',
|
|
60
|
+
uploadUrl: 'string',
|
|
61
|
+
'aria-label': 'string',
|
|
62
|
+
'aria-describedby': 'string',
|
|
63
|
+
} as const;
|
|
64
|
+
static readonly strictContract = false;
|
|
65
|
+
|
|
66
|
+
contract = input.required<InteractionContract<FileInputProps>>();
|
|
67
|
+
vc = viewChild.required('vc', { read: ViewContainerRef });
|
|
68
|
+
inputRef = viewChild<ElementRef<HTMLInputElement>>('inputEl');
|
|
69
|
+
|
|
70
|
+
fileSelect = output<File[]>();
|
|
71
|
+
fileRemove = output<File>();
|
|
72
|
+
fileError = output<string>();
|
|
73
|
+
clear = output<void>();
|
|
74
|
+
focus = output<FocusEvent>();
|
|
75
|
+
blur = output<FocusEvent>();
|
|
76
|
+
uploadStart = output<void>();
|
|
77
|
+
uploadProgress = output<number>();
|
|
78
|
+
uploadSuccess = output<unknown>();
|
|
79
|
+
uploadError = output<unknown>();
|
|
80
|
+
|
|
81
|
+
private http = inject(HttpClient, { optional: true });
|
|
82
|
+
|
|
83
|
+
protected props = computed(() => {
|
|
84
|
+
return coerceContractProps<FileInputProps>(
|
|
85
|
+
this.contract().props,
|
|
86
|
+
FileInputComponent.contractSchema
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
computedMultiple = computed(() => !!this.props()?.multiple);
|
|
91
|
+
computedAccept = computed(() => this.props()?.accept ?? '*/*');
|
|
92
|
+
computedMaxFileSize = computed(() => this.props()?.maxFileSize);
|
|
93
|
+
computedUploadUrl = computed(() => this.props()?.uploadUrl);
|
|
94
|
+
computedAriaLabel = computed(() => this.props()?.['aria-label'] || null);
|
|
95
|
+
computedAriaDescribedBy = computed(() => this.props()?.['aria-describedby'] || null);
|
|
96
|
+
|
|
97
|
+
disabled = false;
|
|
98
|
+
files: File[] = [];
|
|
99
|
+
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
101
|
+
private onChange: (value: File[]) => void = () => {};
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
103
|
+
private onTouched: () => void = () => {};
|
|
104
|
+
|
|
105
|
+
writeValue(obj: unknown): void {
|
|
106
|
+
if (!obj) {
|
|
107
|
+
this.files = [];
|
|
108
|
+
const inputEl = this.inputRef()?.nativeElement;
|
|
109
|
+
if (inputEl) inputEl.value = '';
|
|
110
|
+
} else if (Array.isArray(obj)) {
|
|
111
|
+
this.files = obj;
|
|
112
|
+
} else {
|
|
113
|
+
this.files = [obj as File];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
registerOnChange(fn: (value: File[]) => void): void {
|
|
118
|
+
this.onChange = fn;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
registerOnTouched(fn: () => void): void {
|
|
122
|
+
this.onTouched = fn;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setDisabledState?(isDisabled: boolean): void {
|
|
126
|
+
this.disabled = isDisabled;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onFileChange(event: Event) {
|
|
130
|
+
const inputEl = event.target as HTMLInputElement;
|
|
131
|
+
if (inputEl.files) {
|
|
132
|
+
const selectedFiles = Array.from(inputEl.files);
|
|
133
|
+
if (!selectedFiles.length) return;
|
|
134
|
+
|
|
135
|
+
const maxFileSize = this.computedMaxFileSize();
|
|
136
|
+
if (typeof maxFileSize === 'number' && maxFileSize >= 0) {
|
|
137
|
+
const invalidFiles = selectedFiles.filter(file => file.size > maxFileSize);
|
|
138
|
+
if (invalidFiles.length > 0) {
|
|
139
|
+
this.fileError.emit(`File size exceeds maximum allowed size of ${maxFileSize} bytes`);
|
|
140
|
+
inputEl.value = '';
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.files = this.computedMultiple() ? [...this.files, ...selectedFiles] : [...selectedFiles];
|
|
146
|
+
this.onChange(this.files);
|
|
147
|
+
this.fileSelect.emit(selectedFiles);
|
|
148
|
+
|
|
149
|
+
const uploadUrl = this.computedUploadUrl();
|
|
150
|
+
if (uploadUrl && selectedFiles.length > 0) {
|
|
151
|
+
this.uploadFiles(selectedFiles, uploadUrl);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
inputEl.value = '';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
onFocus(event: FocusEvent) {
|
|
159
|
+
this.focus.emit(event);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
onBlur(event: FocusEvent) {
|
|
163
|
+
this.onTouched();
|
|
164
|
+
this.blur.emit(event);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public removeFile(file: File) {
|
|
168
|
+
this.files = this.files.filter(f => f !== file);
|
|
169
|
+
this.onChange(this.files);
|
|
170
|
+
this.fileRemove.emit(file);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public clearFiles() {
|
|
174
|
+
this.files = [];
|
|
175
|
+
this.onChange(this.files);
|
|
176
|
+
this.clear.emit();
|
|
177
|
+
const inputEl = this.inputRef()?.nativeElement;
|
|
178
|
+
if (inputEl) inputEl.value = '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private uploadFiles(files: File[], url: string) {
|
|
182
|
+
this.uploadStart.emit();
|
|
183
|
+
|
|
184
|
+
if (!this.http) {
|
|
185
|
+
this.uploadError.emit('HttpClient is not provided.');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const formData = new FormData();
|
|
190
|
+
files.forEach(file => formData.append('file', file));
|
|
191
|
+
|
|
192
|
+
this.http
|
|
193
|
+
.post(url, formData, {
|
|
194
|
+
reportProgress: true,
|
|
195
|
+
observe: 'events',
|
|
196
|
+
responseType: 'json',
|
|
197
|
+
})
|
|
198
|
+
.subscribe({
|
|
199
|
+
next: event => {
|
|
200
|
+
if (event.type === HttpEventType.UploadProgress) {
|
|
201
|
+
if (event.total && event.total > 0) {
|
|
202
|
+
const percentComplete = (event.loaded / event.total) * 100;
|
|
203
|
+
this.uploadProgress.emit(percentComplete);
|
|
204
|
+
} else {
|
|
205
|
+
this.uploadProgress.emit(100);
|
|
206
|
+
}
|
|
207
|
+
} else if (event.type === HttpEventType.Response) {
|
|
208
|
+
this.uploadSuccess.emit(event.body);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
error: err => {
|
|
212
|
+
this.uploadError.emit(err.message || 'Upload failed');
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|