native-document 1.0.190 → 1.0.192

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.
@@ -0,0 +1,162 @@
1
+ import {Div, Input, Span, Img, Button, Switch} from '../../../../../elements';
2
+ import { $ } from '../../../../core/data/Observable';
3
+
4
+ import './file-image-preview-mode.css';
5
+
6
+ export default function FileImagePreviewModeRender($desc, nodeInstance) {
7
+ const {$files, fieldDesc, fieldInstance} = $desc.$context;
8
+ const $preview = $(null);
9
+
10
+ const input = buildInput(fieldDesc, fieldInstance, $files, $preview);
11
+
12
+ const variant = $desc.variant || 'hover-overlay';
13
+
14
+ const imagePreview = buildImagePreview($desc, $preview, nodeInstance);
15
+
16
+ fieldInstance.onReset(() => $preview.set(null));
17
+
18
+ if(variant === 'hover-overlay') {
19
+ return buildHoverOverlay($desc, imagePreview, input, $files, nodeInstance);
20
+ }
21
+ if(variant === 'corner-badge') {
22
+ return buildCornerBadge($desc, imagePreview, input, $files, nodeInstance);
23
+ }
24
+ if(variant === 'action-buttons') {
25
+ return buildActionButtons($desc, imagePreview, input, $files, nodeInstance, fieldInstance);
26
+ }
27
+
28
+ return imagePreview;
29
+ }
30
+
31
+ const buildInput = (fieldDesc, fieldInstance, $files, $preview) => {
32
+ const input = Input({
33
+ class: 'field-file-input',
34
+ type: 'file',
35
+ name: fieldDesc.name,
36
+ id: fieldDesc.id || fieldDesc.name,
37
+ accept: fieldDesc.accept || 'image/*',
38
+ disabled: fieldDesc.disabled,
39
+ style: {display: 'none'},
40
+ });
41
+
42
+ fieldInstance.$input = input;
43
+
44
+ input.nd.onChange((e) => {
45
+ const file = e.target.files?.[0];
46
+ if(!file) {
47
+ return;
48
+ }
49
+
50
+ fieldInstance.reset();
51
+ fieldInstance.addFile(file);
52
+ $preview.set(URL.createObjectURL(file));
53
+
54
+ input.value = '';
55
+ });
56
+
57
+ return input;
58
+ };
59
+
60
+
61
+ const buildShapeClass = (instance, defaultClass, $desc) => {
62
+ const props = instance.getEditableProps();
63
+
64
+ props.class.add({
65
+ [defaultClass]: true,
66
+ 'is-circle': ($desc.shape || 'circle') === 'circle',
67
+ 'is-square': ($desc.shape || 'circle') === 'square',
68
+ ['is-'+$desc.mode+'-mode']: true
69
+ });
70
+
71
+ return props.class.value();
72
+ };
73
+
74
+ const buildImagePreview = ($desc, $preview, instance) => {
75
+ const shape = $desc.shape || 'circle';
76
+ const radius = shape === 'circle' ? '50%' : '12px';
77
+
78
+ const props = instance.getEditableProps();
79
+
80
+ if($desc.size) {
81
+ props.style.add('--file-image-preview-size', $desc.size + 'px');
82
+ }
83
+
84
+
85
+ const previewImg = () => Img($preview, {
86
+ class: 'file-image-preview-img',
87
+ alt: $desc.alt || 'Image Preview',
88
+ });
89
+ const placeholder = () => Div({class: 'file-image-preview-placeholder'}, [
90
+ Span({class: 'file-image-preview-placeholder-icon'}, $desc.placeholderIcon),
91
+ ]);
92
+
93
+ return Div({
94
+ class: buildShapeClass(instance,'file-image-preview', $desc),
95
+ style: props.style.value(),
96
+ }, Switch($preview, previewImg, placeholder));
97
+ };
98
+
99
+ const buildHoverOverlay = ($desc, imagePreview, input, $files, nodeInstance) => {
100
+ let overlay = null;
101
+ const props = nodeInstance.getEditableProps();
102
+
103
+ if($desc.ratio) {
104
+ props.style.add('--cover-aspect-ratio', $desc.ratio);
105
+ }
106
+
107
+ if($desc.renderOverlay) {
108
+ overlay = $desc.renderOverlay(imagePreview, $desc, nodeInstance);
109
+ } else {
110
+ overlay = Div({class: 'file-image-preview-overlay'}, [
111
+ Span({class: 'file-image-preview-overlay-icon'}, $desc.overlayIcon || $desc.editImageIcon),
112
+ ]);
113
+ }
114
+
115
+ const wrapper = Div({
116
+ class: buildShapeClass(nodeInstance, 'file-image-preview-wrapper is-hover-overlay', $desc),
117
+ style: props.style.value()
118
+ }, [imagePreview, overlay]);
119
+
120
+ wrapper.nd.onClick(() => input.click());
121
+
122
+ return Div({}, [input, wrapper]);
123
+ };
124
+
125
+ const buildCornerBadge = ($desc, imagePreview, input, instance) => {
126
+ const badge = Div({class: 'file-image-preview-badge'}, $desc.editImageIcon);
127
+ badge.nd.onClick(() => input.click());
128
+
129
+ return Div({}, [
130
+ input,
131
+ Div({
132
+ class: buildShapeClass(instance, 'file-image-preview-wrapper is-corner-badge', $desc),
133
+ }, [imagePreview, badge]),
134
+ ]);
135
+ };
136
+
137
+ const buildActionButtons = ($desc, imagePreview, input, $files, nodeInstance, fieldInstance) => {
138
+ if($desc.renderActions) {
139
+ return $desc.renderActions($desc, imagePreview, () => input.click(), () => removeImagePreview($files, nodeInstance, fieldInstance), nodeInstance);
140
+ }
141
+
142
+ const changeBtn = Button({class: 'file-image-preview-btn is-change'}, $desc.changeLabel);
143
+ changeBtn.nd.onClick(() => input.click());
144
+
145
+ const removeBtn = Button({class: 'file-image-preview-btn is-remove'}, $desc.removeLabel);
146
+ removeBtn.nd.onClick(() => removeImagePreview($files, nodeInstance, fieldInstance));
147
+
148
+ return Div({class: 'file-image-preview-action-wrapper'}, [
149
+ input,
150
+ imagePreview,
151
+ Div({class: 'file-image-preview-actions'}, [changeBtn, removeBtn]),
152
+ ]);
153
+ };
154
+
155
+ const removeImagePreview = ($files, nodeInstance, fieldInstance) => {
156
+ const item = $files.val()?.[0];
157
+ fieldInstance.reset();
158
+ if(item) {
159
+ nodeInstance.emit('remove', item);
160
+ item.emit('remove', item);
161
+ }
162
+ };
@@ -1,16 +1,21 @@
1
1
  :root {
2
- --file-avatar-radius-circle: 50%;
3
- --file-avatar-radius-square: 12px;
4
- --file-avatar-badge-size: 26px;
2
+ --file-image-preview-radius-circle: 50%;
3
+ --file-image-preview-radius-square: 12px;
4
+ --file-image-preview-badge-size: 26px;
5
+ --cover-aspect-ratio: 16 / 9;
6
+ --cover-border-radius: 12px;
7
+ --cover-overlay-bg: rgba(0, 0, 0, 0.5);
8
+ --cover-transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
9
+ --file-image-preview-size: 100%;
5
10
  }
6
11
 
7
- .file-avatar-wrapper {
12
+ .file-image-preview-wrapper {
8
13
  position: relative;
9
14
  display: inline-block;
10
15
  cursor: pointer;
11
16
  }
12
17
 
13
- .file-avatar {
18
+ .file-image-preview {
14
19
  overflow: hidden;
15
20
  background: var(--gray-lite-5);
16
21
  border: 1px solid var(--field-input-border);
@@ -20,11 +25,37 @@
20
25
  flex-shrink: 0;
21
26
  position: relative;
22
27
 
23
- &.is-circle { border-radius: var(--file-avatar-radius-circle); }
24
- &.is-square { border-radius: var(--file-avatar-radius-square); }
28
+ &.is-circle,
29
+ &.is-square {
30
+ }
31
+ &.is-circle {
32
+ border-radius: var(--file-image-preview-radius-circle);
33
+ }
34
+ &.is-square {
35
+ border-radius: var(--file-image-preview-radius-square);
36
+ }
37
+
38
+ &.is-avatar-mode {
39
+ width: var(--file-image-preview-size);
40
+ height: var(--file-image-preview-size);
41
+ }
42
+ &.is-cover-mode {
43
+ position: relative;
44
+ width: var(--file-image-preview-size);
45
+ aspect-ratio: var(--cover-aspect-ratio);
46
+ border-radius: var(--cover-border-radius);
47
+ overflow: hidden;
48
+ background-color: #f3f4f6;
49
+ border: 1px dashed #d1d5db;
50
+ box-sizing: border-box;
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ transition: var(--cover-transition);
55
+ }
25
56
  }
26
57
 
27
- .file-avatar-img {
58
+ .file-image-preview-img {
28
59
  width: 100%;
29
60
  height: 100%;
30
61
  object-fit: cover;
@@ -33,7 +64,7 @@
33
64
  inset: 0;
34
65
  }
35
66
 
36
- .file-avatar-placeholder {
67
+ .file-image-preview-placeholder {
37
68
  display: flex;
38
69
  align-items: center;
39
70
  justify-content: center;
@@ -41,13 +72,13 @@
41
72
  height: 100%;
42
73
  }
43
74
 
44
- .file-avatar-placeholder-icon {
75
+ .file-image-preview-placeholder-icon {
45
76
  font-size: 36px;
46
77
  }
47
78
 
48
79
  /* Hover overlay variant */
49
- .file-avatar-wrapper.is-hover-overlay {
50
- .file-avatar-overlay {
80
+ .file-image-preview-wrapper.is-hover-overlay {
81
+ .file-image-preview-overlay {
51
82
  position: absolute;
52
83
  inset: 0;
53
84
  background: rgba(0, 0, 0, 0.4);
@@ -58,26 +89,26 @@
58
89
  transition: opacity 0.15s ease;
59
90
  }
60
91
 
61
- &.is-circle .file-avatar-overlay { border-radius: var(--file-avatar-radius-circle); }
62
- &.is-square .file-avatar-overlay { border-radius: var(--file-avatar-radius-square); }
92
+ &.is-circle .file-image-preview-overlay { border-radius: var(--file-image-preview-radius-circle); }
93
+ &.is-square .file-image-preview-overlay { border-radius: var(--file-image-preview-radius-square); }
63
94
 
64
- &:hover .file-avatar-overlay {
95
+ &:hover .file-image-preview-overlay {
65
96
  opacity: 1;
66
97
  }
67
98
 
68
- .file-avatar-overlay-icon {
99
+ .file-image-preview-overlay-icon {
69
100
  font-size: 24px;
70
101
  }
71
102
  }
72
103
 
73
104
  /* Corner badge variant */
74
- .file-avatar-wrapper.is-corner-badge {
75
- .file-avatar-badge {
105
+ .file-image-preview-wrapper.is-corner-badge {
106
+ .file-image-preview-badge {
76
107
  position: absolute;
77
108
  bottom: 0;
78
109
  right: 0;
79
- width: var(--file-avatar-badge-size);
80
- height: var(--file-avatar-badge-size);
110
+ width: var(--file-image-preview-badge-size);
111
+ height: var(--file-image-preview-badge-size);
81
112
  border-radius: 50%;
82
113
  background: var(--background);
83
114
  border: 1px solid var(--field-input-border);
@@ -95,19 +126,19 @@
95
126
  }
96
127
 
97
128
  /* Action buttons variant */
98
- .file-avatar-action-wrapper {
129
+ .file-image-preview-action-wrapper {
99
130
  display: flex;
100
131
  align-items: center;
101
132
  gap: var(--space-comfortable);
102
133
  }
103
134
 
104
- .file-avatar-actions {
135
+ .file-image-preview-actions {
105
136
  display: flex;
106
137
  flex-direction: column;
107
138
  gap: var(--space-cozy);
108
139
  }
109
140
 
110
- .file-avatar-btn {
141
+ .file-image-preview-btn {
111
142
  display: inline-flex;
112
143
  align-items: center;
113
144
  padding: var(--space-cozy) var(--space-comfortable);
@@ -18,7 +18,7 @@ export { default as FileNativeModeRender } from './file-upload-mode/FileNativeMo
18
18
  export { default as FileDropzoneModeRender } from './file-upload-mode/FileDropzoneModeRender';
19
19
  export { default as FileWallModeRender } from './file-upload-mode/FileWallModeRender';
20
20
  export { default as FileUploadButtonModeRender } from './file-upload-mode/FileUploadButtonModeRender';
21
- export { default as FileAvatarModeRender } from './file-upload-mode/FileAvatarModeRender';
21
+ export { default as FileImagePreviewModeRender } from './file-upload-mode/FileImagePreviewModeRender';
22
22
  export { default as DateFieldRender } from './fields/DateFieldRender';
23
23
  export { default as TimeFieldRender } from './fields/TimeFieldRender';
24
24
  export { default as SliderFieldRender } from './fields/SliderFieldRender';
@@ -232,7 +232,7 @@ export type ObservableObject<T extends Record<string, any>> = ObservableItem<T>
232
232
  $val(): T;
233
233
  val(): T;
234
234
  get(key: string): any;
235
- only<K extends keyof T>(...properties: K[]): Pick<T, K>;
235
+ only<K extends keyof T>(properties: K[]): Pick<T, K>;
236
236
  $get(key: string): any;
237
237
  set(values: Partial<T>): void;
238
238
  $set(values: Partial<T>): void;
@@ -1,183 +0,0 @@
1
- import BaseComponent from '../../../../BaseComponent';
2
- import HasEventEmitter from '../../../../../core/utils/HasEventEmitter';
3
-
4
- /**
5
- * Avatar-style single file upload mode for FileField.
6
- * Displays a circular or square avatar with hover/badge/button interaction variants.
7
- * Ideal for profile picture uploads.
8
- * @example
9
- * new FileField('avatar')
10
- * .mode('avatar');
11
- *
12
- * FileAvatarMode.use((description, instance) => {
13
- * // description.variant — 'hover-overlay' | 'corner-badge' | 'action-buttons'
14
- * // description.shape — 'circle' | 'square'
15
- * // description.size, description.placeholderIcon, description.overlayIcon,
16
- * // description.editImageIcon, description.changeLabel, description.removeLabel,
17
- * // description.renderAvatar, description.renderOverlay, description.renderActions
18
- * return Div({ class: \`avatar-upload avatar-upload--\${description.variant}\`,
19
- * style: { width: description.size, height: description.size } });
20
- * });
21
- *
22
- * @constructor
23
- * @param {GlobalAttributes} [props={}]
24
- */
25
- export default function FileAvatarMode(props = {}) {
26
- if(!(this instanceof FileAvatarMode)) {
27
- return new FileAvatarMode(props);
28
- }
29
-
30
- BaseComponent.call(this, props);
31
-
32
- this.$description = {
33
- variant: 'hover-overlay',
34
- shape: 'circle',
35
- size: 100,
36
- placeholderIcon: null,
37
- overlayIcon: null,
38
- editImageIcon: null,
39
- changeLabel: 'Change photo',
40
- removeLabel: 'Remove',
41
- renderAvatar: null,
42
- renderOverlay: null,
43
- renderActions: null,
44
- props,
45
- };
46
- }
47
-
48
- BaseComponent.extends(FileAvatarMode);
49
- BaseComponent.use(FileAvatarMode, HasEventEmitter);
50
-
51
- FileAvatarMode.defaultTemplate = null;
52
-
53
- /**
54
- * Registers the render template for FileAvatarMode.
55
- * @param {(description: {
56
- * variant: 'hover-overlay'|'corner-badge'|'action-buttons',
57
- * shape: 'circle'|'square',
58
- * size: number|string,
59
- * placeholderIcon: NdChild|null,
60
- * overlayIcon: NdChild|null,
61
- * editImageIcon: NdChild|null,
62
- * changeLabel: NdChild,
63
- * removeLabel: NdChild,
64
- * renderAvatar: ((desc: *, instance: FileAvatarMode) => NdChild)|null,
65
- * renderOverlay: ((desc: *, instance: FileAvatarMode) => NdChild)|null,
66
- * renderActions: ((desc: *, instance: FileAvatarMode) => NdChild)|null,
67
- * props: GlobalAttributes
68
- * }, instance: FileAvatarMode) => NdChild} template
69
- */
70
- FileAvatarMode.use = function(template) {
71
- FileAvatarMode.defaultTemplate = template;
72
- };
73
-
74
- /** @returns {this} */
75
- FileAvatarMode.prototype.hoverOverlay = function() {
76
- this.$description.variant = 'hover-overlay';
77
- return this;
78
- };
79
-
80
- /** @returns {this} */
81
- FileAvatarMode.prototype.cornerBadge = function() {
82
- this.$description.variant = 'corner-badge';
83
- return this;
84
- };
85
-
86
- /** @returns {this} */
87
- FileAvatarMode.prototype.actionButtons = function() {
88
- this.$description.variant = 'action-buttons';
89
- return this;
90
- };
91
-
92
- /** @returns {this} */
93
- FileAvatarMode.prototype.circle = function() {
94
- this.$description.shape = 'circle';
95
- return this;
96
- };
97
-
98
- /** @returns {this} */
99
- FileAvatarMode.prototype.square = function() {
100
- this.$description.shape = 'square';
101
- return this;
102
- };
103
-
104
- /**
105
- * @param {number|string} size - Size in pixels or CSS value
106
- * @returns {this}
107
- */
108
- FileAvatarMode.prototype.size = function(size) {
109
- this.$description.size = size;
110
- return this;
111
- };
112
-
113
- /**
114
- * @param {NdChild} icon
115
- * @returns {this}
116
- */
117
- FileAvatarMode.prototype.placeholderIcon = function(icon) {
118
- this.$description.placeholderIcon = icon;
119
- return this;
120
- };
121
-
122
- /**
123
- * @param {NdChild} icon
124
- * @returns {this}
125
- */
126
- FileAvatarMode.prototype.overlayIcon = function(icon) {
127
- this.$description.overlayIcon = icon;
128
- return this;
129
- };
130
-
131
- /**
132
- * @param {NdChild} icon
133
- * @returns {this}
134
- */
135
- FileAvatarMode.prototype.editImageIcon = function(icon) {
136
- this.$description.editImageIcon = icon;
137
- return this;
138
- };
139
-
140
- /**
141
- * @param {NdChild} label
142
- * @returns {this}
143
- */
144
- FileAvatarMode.prototype.changeLabel = function(label) {
145
- this.$description.changeLabel = label;
146
- return this;
147
- };
148
-
149
- /**
150
- * @param {NdChild} label
151
- * @returns {this}
152
- */
153
- FileAvatarMode.prototype.removeLabel = function(label) {
154
- this.$description.removeLabel = label;
155
- return this;
156
- };
157
-
158
- /**
159
- * @param {(desc: *, instance: FileAvatarMode) => NdChild} fn
160
- * @returns {this}
161
- */
162
- FileAvatarMode.prototype.renderAvatar = function(fn) {
163
- this.$description.renderAvatar = fn;
164
- return this;
165
- };
166
-
167
- /**
168
- * @param {(desc: *, instance: FileAvatarMode) => NdChild} fn
169
- * @returns {this}
170
- */
171
- FileAvatarMode.prototype.renderOverlay = function(fn) {
172
- this.$description.renderOverlay = fn;
173
- return this;
174
- };
175
-
176
- /**
177
- * @param {(desc: *, instance: FileAvatarMode) => NdChild} fn
178
- * @returns {this}
179
- */
180
- FileAvatarMode.prototype.renderActions = function(fn) {
181
- this.$description.renderActions = fn;
182
- return this;
183
- };
@@ -1,46 +0,0 @@
1
- import type { ValidChild } from '../../../../../types/elements';
2
- import type { GlobalAttributes } from '../../../../../types/globals';
3
- import type { BaseComponent } from '../../../BaseComponent';
4
-
5
- export type FileAvatarModeDescription = {
6
- variant: 'hover-overlay' | 'corner-badge' | 'action-buttons';
7
- shape: 'circle' | 'square';
8
- size: number | string;
9
- placeholderIcon: ValidChild | null;
10
- overlayIcon: ValidChild | null;
11
- editImageIcon: ValidChild | null;
12
- changeLabel: ValidChild;
13
- removeLabel: ValidChild;
14
- renderAvatar: ((desc: unknown, instance: FileAvatarModeInterface) => ValidChild) | null;
15
- renderOverlay: ((desc: unknown, instance: FileAvatarModeInterface) => ValidChild) | null;
16
- renderActions: ((desc: unknown, instance: FileAvatarModeInterface) => ValidChild) | null;
17
- props: GlobalAttributes;
18
- };
19
-
20
- export interface FileAvatarModeInterface extends BaseComponent {
21
- render(renderFn: (description: FileAvatarModeDescription, instance: FileAvatarModeInterface) => ValidChild): this;
22
- hoverOverlay(): this;
23
- cornerBadge(): this;
24
- actionButtons(): this;
25
- circle(): this;
26
- square(): this;
27
- size(size: number | string): this;
28
- placeholderIcon(icon: ValidChild): this;
29
- overlayIcon(icon: ValidChild): this;
30
- editImageIcon(icon: ValidChild): this;
31
- changeLabel(label: ValidChild): this;
32
- removeLabel(label: ValidChild): this;
33
- renderAvatar(fn: (desc: unknown, instance: FileAvatarModeInterface) => ValidChild): this;
34
- renderOverlay(fn: (desc: unknown, instance: FileAvatarModeInterface) => ValidChild): this;
35
- renderActions(fn: (desc: unknown, instance: FileAvatarModeInterface) => ValidChild): this;
36
- }
37
-
38
-
39
- export declare function FileAvatarMode(props?: GlobalAttributes): FileAvatarModeInterface;
40
- export declare namespace FileAvatarMode {
41
-
42
-
43
- function use(template: (description: FileAvatarModeDescription, instance: FileAvatarModeInterface) => ValidChild): void;
44
-
45
-
46
- }