1 |
|
2 |
|
3 |
|
4 | import angular from 'angular';
|
5 | import 'dom4';
|
6 | import '../form/form.scss';
|
7 | import '../save-field-ng/save-field-ng.scss';
|
8 |
|
9 | import '../loader-inline/loader-inline';
|
10 | import ButtonSet from '../button-set-ng/button-set-ng';
|
11 | import MessageBundle from '../message-bundle-ng/message-bundle-ng';
|
12 | import Form from '../form-ng/form-ng';
|
13 | import Shortcuts from '../shortcuts-ng/shortcuts-ng';
|
14 | import Button from '../button-ng/button-ng';
|
15 | import PromisedClick from '../promised-click-ng/promised-click-ng';
|
16 |
|
17 | const angularModule = angular.module('Ring.save-field', [
|
18 | MessageBundle,
|
19 |
|
20 | |
21 |
|
22 |
|
23 | Form,
|
24 | Shortcuts,
|
25 | Button,
|
26 | ButtonSet,
|
27 | PromisedClick
|
28 | ]);
|
29 |
|
30 | angularModule.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 |
|
56 | angularModule.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);
|
128 |
|
129 | if (scope.afterSave) {
|
130 | if (draftMode) {
|
131 | return $timeout(afterSaveCall);
|
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 |
|
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 |
|
335 |
|
336 |
|
337 |
|
338 |
|
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 |
|
357 | export default angularModule.name;
|