native-document 1.0.189 → 1.0.191

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.
@@ -95,16 +95,19 @@ export default function NativeFetch($baseUrl) {
95
95
  };
96
96
 
97
97
  export const resolveData = (data) => {
98
- if(data.__$Observable) {
98
+ if(data?.__$Observable) {
99
99
  return data.resolve();
100
100
  }
101
101
  for(const key in data) {
102
102
  const value = data[key];
103
+ if(value == null) {
104
+ continue;
105
+ }
103
106
  if(value.__$Observable) {
104
107
  data[key] = value.resolve();
105
108
  continue;
106
109
  }
107
- if(typeof data[key] === 'object') {
110
+ if(typeof value === 'object') {
108
111
  data[key] = resolveData(data[key]);
109
112
  }
110
113
  }
@@ -26,9 +26,7 @@ export default function NumberFieldRender($desc, instance) {
26
26
 
27
27
  const el = FieldRender($desc, instance);
28
28
 
29
- if($desc.decimals != null) {
30
- setupDecimalsHandling(instance, $desc);
31
- }
29
+ setupDecimalsHandling(instance, $desc);
32
30
 
33
31
  return el;
34
32
  }
@@ -40,9 +38,9 @@ const setupDecimalsHandling = (instance, $desc) => {
40
38
  }
41
39
 
42
40
  input.nd.onBlur(() => {
43
- const raw = parseFloat(input.value);
41
+ const raw = Number(input.value);
44
42
  if(!isNaN(raw)) {
45
- const formatted = raw.toFixed($desc.decimals);
43
+ const formatted = $desc.decimals ? raw.toFixed($desc.decimals) : raw;
46
44
  input.value = formatted;
47
45
  if($desc.value?.__$isObservable) {
48
46
  $desc.value.set(parseFloat(formatted));
@@ -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: initial;
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,6 +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
236
  $get(key: string): any;
236
237
  set(values: Partial<T>): void;
237
238
  $set(values: Partial<T>): void;
@@ -319,6 +320,8 @@ export interface ObservableStatic {
319
320
  object<T extends Record<string, any>>(value: T, configs?: ObservableConfig | null): ObservableObject<T>;
320
321
  json<T extends Record<string, any>>(value: T, configs?: ObservableConfig | null): ObservableObject<T>;
321
322
 
323
+ auto<T>(value: T): ObservableArray<T>|ObservableObject<T>|ObservableItem<T>;
324
+
322
325
  resource<T = any>(
323
326
  fn: (...args: any[]) => Promise<T>,
324
327
  dependencies?: ValidComputedDependencies,
@@ -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
- }