UNPKG

9.87 kBJavaScriptView Raw
1/**
2 * @name Save Field Ng
3 */
4import angular from 'angular';
5import 'dom4';
6import '../form/form.scss';
7import '../save-field-ng/save-field-ng.scss';
8
9import '../loader-inline/loader-inline';
10import ButtonSet from '../button-set-ng/button-set-ng';
11import MessageBundle from '../message-bundle-ng/message-bundle-ng';
12import Form from '../form-ng/form-ng';
13import Shortcuts from '../shortcuts-ng/shortcuts-ng';
14import Button from '../button-ng/button-ng';
15import PromisedClick from '../promised-click-ng/promised-click-ng';
16
17const angularModule = angular.module('Ring.save-field', [
18 MessageBundle,
19
20 /**
21 * for error-bubble
22 */
23 Form,
24 Shortcuts,
25 Button,
26 ButtonSet,
27 PromisedClick
28]);
29
30angularModule.constant('rgSaveFieldShortcutsMode', {
31 id: 'ring-save-field',
32 shortcuts: [
33 {
34 key: 'ctrl+enter',
35 action: 'comboSubmit'
36 },
37 {
38 key: 'enter',
39 action: 'submit'
40 },
41 {
42 key: 'esc',
43 action: 'cancel'
44 },
45 {
46 key: 'up',
47 action: 'noop'
48 },
49 {
50 key: 'down',
51 action: 'noop'
52 }
53 ]
54});
55
56angularModule.directive(
57 'rgSaveField',
58 function rgSaveFieldDirective(RingMessageBundle, $timeout, $q, $compile, $parse) {
59 const MULTI_LINE_SPLIT_PATTERN = /(\r\n|\n|\r)/gm;
60 const MULTI_LINE_LIST_MODE = 'list';
61 const CUSTOM_ERROR_ID = 'customError';
62 const ERROR_DESCRIPTION = 'error_description';
63 const ERROR_DEVELOPER_MSG = 'error_developer_message';
64
65 return {
66 require: 'rgSaveField',
67 transclude: true,
68 template: require('./save-field-ng.html'),
69 scope: {
70 api: '=?',
71 value: '=',
72 workingValue: '=',
73 onSave: '&',
74 afterSave: '&?',
75 validate: '&?',
76 parseElement: '&?',
77 formatElement: '&?',
78 multiline: '@'
79 },
80 link: function link(scope, iElem, iAttrs, ctrl) {
81 const placeholder = angular.element(
82 iElem[0].querySelector('.ring-save-field__transclude-placeholder')
83 );
84 $compile(
85 angular.element('<div rg-error-bubble="saveFieldForm"></div>')
86 )(scope, errorBubble => {
87 placeholder.append(errorBubble);
88 });
89
90 const customError = {
91 message: ''
92 };
93
94 let blurTimeout = null;
95 let isTextarea = false;
96
97 const draftMode = iAttrs.workingValue;
98 const valueField = draftMode ? 'workingValue' : 'value';
99
100 function submitChanges() {
101 if (
102 !scope.saveFieldForm.$valid ||
103 scope.loading ||
104 angular.equals(scope.initial, scope[valueField])
105 ) {
106 return false;
107 }
108
109 function afterSaveCall() {
110 return $q.when(scope.afterSave({
111 value: scope[valueField]
112 }));
113 }
114
115 function success() {
116 scope.initial = angular.copy(scope[valueField]);
117 scope.saveFieldForm.$setPristine();
118
119 scope.done = true;
120
121 if (draftMode) {
122 scope.value = scope.workingValue;
123 }
124
125 $timeout(() => {
126 scope.done = false;
127 }, 1000); //eslint-disable-line no-magic-numbers
128
129 if (scope.afterSave) {
130 if (draftMode) {
131 return $timeout(afterSaveCall); // we need digest to sync value before calling after save
132 } else {
133 return afterSaveCall();
134 }
135 }
136
137 return undefined;
138 }
139
140 function error(err) {
141 let message;
142 if (typeof err === 'string') {
143 message = err;
144 } else if (typeof err === 'object') {
145 const errorData = err.data || err;
146 message = errorData[ERROR_DESCRIPTION] || errorData[ERROR_DEVELOPER_MSG];
147 }
148
149 customError.message = message;
150 scope.saveFieldForm.$setValidity(CUSTOM_ERROR_ID, false, customError);
151 }
152
153 scope.cancelBlur();
154
155 scope.loading = true;
156
157 let onsave = ctrl.getSave();
158 if (onsave) {
159 onsave = $q.when(onsave(scope[valueField]));
160 } else {
161 onsave = $q.when(scope.onSave({
162 value: scope[valueField]
163 }));
164 }
165
166 return onsave.
167 then(success, error).
168 then(() => {
169 scope.loading = false;
170 });
171 }
172
173 function resetValue() {
174 if (scope.loading) {
175 return;
176 }
177
178 scope.$evalAsync(() => {
179 scope[valueField] = scope.initial ? scope.initial : '';
180 scope.saveFieldForm.$setValidity(CUSTOM_ERROR_ID, true, customError);
181 scope.saveFieldForm.$setPristine();
182 });
183 }
184
185 function addMultilineProcessing(controlName) {
186 const stopWatch = scope.$watch(`saveFieldForm.${controlName}`, control => {
187 if (!control || !control.$formatters || !control.$parsers) {
188 return;
189 }
190
191 control.$formatters.push(value => {
192 if (!value) {
193 return value;
194 }
195
196 let formattedValue;
197 if (iAttrs.formatElement) {
198 formattedValue = value.map(element => scope.formatElement({element}));
199 } else {
200 formattedValue = value;
201 }
202
203 return formattedValue.join('\n');
204 });
205
206 control.$parsers.push(value => {
207 let array = value && value.split(MULTI_LINE_SPLIT_PATTERN) || [];
208
209 function notEmpty(val) {
210 return val && val.trim() && val !== '\n';
211 }
212
213 array = array.filter(notEmpty);
214
215 if (iAttrs.parseElement) {
216 array = array.map(element => scope.parseElement({element: element.trim()}));
217 }
218
219 return array;
220 });
221
222 stopWatch();
223 });
224 }
225
226 scope.cancelBlur = () => {
227 $timeout(() => {
228 if (blurTimeout) {
229 $timeout.cancel(blurTimeout);
230 blurTimeout = null;
231 }
232 // eslint-disable-next-line no-magic-numbers
233 }, 10);
234 };
235
236 if (draftMode) {
237 scope.$watch('value', value => {
238 scope.workingValue = angular.copy(value);
239 scope.initial = value;
240 });
241 }
242
243 scope.$watch(valueField, value => {
244 let promise = null;
245 if (scope.saveFieldForm.$pristine) {
246 scope.initial = value;
247 } else if (scope.initial && angular.equals(scope.initial, value)) {
248 resetValue();
249 } else if (scope.validate) {
250 promise = scope.validate({
251 value
252 });
253 }
254
255 $q.when(promise).
256 then(error => {
257 if (error) {
258 return $q.reject(error);
259 } else {
260 customError.message = '';
261 scope.saveFieldForm.$setValidity(CUSTOM_ERROR_ID, true, customError);
262
263 return undefined;
264 }
265 }).
266 catch(error => {
267 customError.message = error;
268 scope.saveFieldForm.$setValidity(CUSTOM_ERROR_ID, false, customError);
269 });
270 });
271
272 let inputNode = iElem[0].querySelector('input, .ring-save-field__input');
273
274 if (!inputNode) {
275 inputNode = iElem[0].querySelector('textarea');
276 isTextarea = !!inputNode;
277 }
278
279 if (inputNode) {
280 inputNode.classList.add('ring-js-shortcuts');
281
282 inputNode.addEventListener('focus', () => {
283 scope.$evalAsync(() => {
284 scope.focus = true;
285 });
286 });
287
288 inputNode.addEventListener('blur', () => {
289 scope.$evalAsync(() => {
290 scope.focus = false;
291 });
292 });
293
294 if (isTextarea && scope.multiline === MULTI_LINE_LIST_MODE) {
295 addMultilineProcessing(inputNode.name);
296 }
297 }
298
299 scope.wording = {
300 save: RingMessageBundle.form_save(),
301 saved: RingMessageBundle.form_saved(),
302 cancel: RingMessageBundle.form_cancel()
303 };
304
305 scope.keyMap = {
306 comboSubmit: e => {
307 if (isTextarea) {
308 e.preventDefault();
309 submitChanges();
310 }
311 },
312 submit: e => {
313 if (!isTextarea) {
314 e.preventDefault();
315 submitChanges();
316 }
317 },
318 cancel: resetValue,
319 noop: angular.noop
320 };
321
322 scope.api = {
323 save: submitChanges,
324 cancel: resetValue
325 };
326
327 scope.submitChanges = ctrl.submitChanges = submitChanges;
328
329 scope.cancelChanges = ctrl.cancelChanges = resetValue;
330
331 scope.focus = false;
332
333 scope.$on('$destroy', () => {
334 // 1) Bindings are already disabled by this time, so replacing scope.value = ... has no effect
335 // 2) We can't use scope.value.someField because we don't know anything about scope.value, it's passed from the outside
336 // 3) Probably we can use controllerAs to add one more object layer (ctrl.value) so the JS linking would work
337 // but errorBubble works with scope only, so a large refactoring of rgSaveField and other components is needed.
338 // This is the simplest solution:
339 if (iAttrs.value) {
340 $parse(iAttrs.value).assign(scope.$parent, scope.initial);
341 }
342 });
343 },
344 controller() {
345 let onSave = null;
346
347 this.setSave = cb => {
348 onSave = cb;
349 };
350
351 this.getSave = () => onSave;
352 }
353 };
354 }
355);
356
357export default angularModule.name;