Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/webroot/rsrc/externals/javelin/lib/Workflow.js
12242 views
1
/**
2
* @requires javelin-stratcom
3
* javelin-request
4
* javelin-dom
5
* javelin-vector
6
* javelin-install
7
* javelin-util
8
* javelin-mask
9
* javelin-uri
10
* javelin-routable
11
* @provides javelin-workflow
12
* @javelin
13
*/
14
15
JX.install('Workflow', {
16
construct : function(uri, data) {
17
if (__DEV__) {
18
if (!uri || uri == '#') {
19
JX.$E(
20
'new JX.Workflow(<?>, ...): '+
21
'bogus URI provided when creating workflow.');
22
}
23
}
24
this.setURI(uri);
25
this.setData(data || {});
26
},
27
28
events : ['error', 'finally', 'submit', 'start'],
29
30
statics : {
31
_stack : [],
32
newFromForm : function(form, data, keep_enabled) {
33
var pairs = JX.DOM.convertFormToListOfPairs(form);
34
for (var k in data) {
35
pairs.push([k, data[k]]);
36
}
37
38
var inputs;
39
if (keep_enabled) {
40
inputs = [];
41
} else {
42
// Disable form elements during the request
43
inputs = [].concat(
44
JX.DOM.scry(form, 'input'),
45
JX.DOM.scry(form, 'button'),
46
JX.DOM.scry(form, 'textarea'));
47
for (var ii = 0; ii < inputs.length; ii++) {
48
if (inputs[ii].disabled) {
49
delete inputs[ii];
50
} else {
51
inputs[ii].disabled = true;
52
}
53
}
54
}
55
56
var workflow = new JX.Workflow(form.getAttribute('action'), {});
57
58
workflow._form = form;
59
60
workflow.setDataWithListOfPairs(pairs);
61
workflow.setMethod(form.getAttribute('method'));
62
63
var onfinally = JX.bind(workflow, function() {
64
if (!this._keepControlsDisabled) {
65
for (var ii = 0; ii < inputs.length; ii++) {
66
inputs[ii] && (inputs[ii].disabled = false);
67
}
68
}
69
});
70
workflow.listen('finally', onfinally);
71
72
return workflow;
73
},
74
newFromLink : function(link) {
75
var workflow = new JX.Workflow(link.href);
76
return workflow;
77
},
78
79
_push : function(workflow) {
80
JX.Mask.show();
81
JX.Workflow._stack.push(workflow);
82
},
83
_pop : function() {
84
var dialog = JX.Workflow._stack.pop();
85
(dialog.getCloseHandler() || JX.bag)();
86
dialog._destroy();
87
JX.Mask.hide();
88
},
89
_onlink: function(event) {
90
// See T13302. When a user clicks a link in a dialog and that link
91
// triggers a navigation event, we want to close the dialog as though
92
// they had pressed a button.
93
94
// When Quicksand is enabled, this is particularly relevant because
95
// the dialog will stay in the foreground while the page content changes
96
// in the background if we do not dismiss the dialog.
97
98
// If this is a Command-Click, the link will open in a new window.
99
var is_command = !!event.getRawEvent().metaKey;
100
if (is_command) {
101
return;
102
}
103
104
var link = event.getNode('tag:a');
105
106
// If the link is an anchor, or does not go anywhere, ignore the event.
107
var href = link.getAttribute('href');
108
if (typeof href !== 'string') {
109
return;
110
}
111
112
if (!href.length || href[0] === '#') {
113
return;
114
}
115
116
// This link will open in a new window.
117
if (link.target === '_blank') {
118
return;
119
}
120
121
// This link is really a dialog button which we'll handle elsewhere.
122
if (JX.Stratcom.hasSigil(link, 'jx-workflow-button')) {
123
return;
124
}
125
126
// Close the dialog.
127
JX.Workflow._pop();
128
},
129
_onbutton : function(event) {
130
131
if (JX.Stratcom.pass()) {
132
return;
133
}
134
135
// Get the button (which is sometimes actually another tag, like an <a />)
136
// which triggered the event. In particular, this makes sure we get the
137
// right node if there is a <button> with an <img /> inside it or
138
// or something similar.
139
var t = event.getNode('jx-workflow-button') ||
140
event.getNode('tag:button');
141
142
// If this button disables workflow (normally, because it is a file
143
// download button) let the event through without modification.
144
if (JX.Stratcom.getData(t).disableWorkflow) {
145
return;
146
}
147
148
event.prevent();
149
150
if (t.name == '__cancel__' || t.name == '__close__') {
151
JX.Workflow._pop();
152
} else {
153
var form = event.getNode('jx-dialog');
154
JX.Workflow._dosubmit(form, t);
155
}
156
},
157
_onsyntheticsubmit : function(e) {
158
if (JX.Stratcom.pass()) {
159
return;
160
}
161
e.prevent();
162
var form = e.getNode('jx-dialog');
163
var button = JX.DOM.find(form, 'button', '__default__');
164
JX.Workflow._dosubmit(form, button);
165
},
166
_dosubmit : function(form, button) {
167
// Issue a DOM event first, so form-oriented handlers can act.
168
var dom_event = JX.DOM.invoke(form, 'didWorkflowSubmit');
169
if (dom_event.getPrevented()) {
170
return;
171
}
172
173
var data = JX.DOM.convertFormToListOfPairs(form);
174
data.push([button.name, button.value || true]);
175
176
var active = JX.Workflow._getActiveWorkflow();
177
178
active._form = form;
179
180
var e = active.invoke('submit', {form: form, data: data});
181
if (!e.getStopped()) {
182
// NOTE: Don't remove the current dialog yet because additional
183
// handlers may still want to access the nodes.
184
185
// Disable whatever button the user clicked to prevent duplicate
186
// submission mistakes when you accidentally click a button multiple
187
// times. See T11145.
188
button.disabled = true;
189
190
active
191
.setURI(form.getAttribute('action') || active.getURI())
192
.setDataWithListOfPairs(data)
193
.start();
194
}
195
},
196
_getActiveWorkflow : function() {
197
var stack = JX.Workflow._stack;
198
return stack[stack.length - 1];
199
},
200
201
_onresizestart: function(e) {
202
var self = JX.Workflow;
203
if (self._resizing) {
204
return;
205
}
206
207
var workflow = self._getActiveWorkflow();
208
if (!workflow) {
209
return;
210
}
211
212
e.kill();
213
214
var form = JX.DOM.find(workflow._root, 'div', 'jx-dialog');
215
var resize = e.getNodeData('jx-dialog-resize');
216
var node_y = JX.$(resize.resizeY);
217
218
var dim = JX.Vector.getDim(form);
219
dim.y = JX.Vector.getDim(node_y).y;
220
221
if (!form._minimumSize) {
222
form._minimumSize = dim;
223
}
224
225
self._resizing = {
226
min: form._minimumSize,
227
form: form,
228
startPos: JX.$V(e),
229
startDim: dim,
230
resizeY: node_y,
231
resizeX: resize.resizeX
232
};
233
},
234
235
_onmousemove: function(e) {
236
var self = JX.Workflow;
237
if (!self._resizing) {
238
return;
239
}
240
241
var spec = self._resizing;
242
var form = spec.form;
243
var min = spec.min;
244
245
var delta = JX.$V(e).add(-spec.startPos.x, -spec.startPos.y);
246
var src_dim = spec.startDim;
247
var dst_dim = JX.$V(src_dim.x + delta.x, src_dim.y + delta.y);
248
249
if (dst_dim.x < min.x) {
250
dst_dim.x = min.x;
251
}
252
253
if (dst_dim.y < min.y) {
254
dst_dim.y = min.y;
255
}
256
257
if (spec.resizeX) {
258
JX.$V(dst_dim.x, null).setDim(form);
259
}
260
261
if (spec.resizeY) {
262
JX.$V(null, dst_dim.y).setDim(spec.resizeY);
263
}
264
},
265
266
_onmouseup: function() {
267
var self = JX.Workflow;
268
if (!self._resizing) {
269
return;
270
}
271
272
self._resizing = false;
273
}
274
},
275
276
members : {
277
_root : null,
278
_pushed : false,
279
_data : null,
280
281
_form: null,
282
_paused: 0,
283
_nextCallback: null,
284
_keepControlsDisabled: false,
285
286
getSourceForm: function() {
287
return this._form;
288
},
289
290
pause: function() {
291
this._paused++;
292
return this;
293
},
294
295
resume: function() {
296
if (!this._paused) {
297
JX.$E('Resuming a workflow which is not paused!');
298
}
299
300
this._paused--;
301
302
if (!this._paused) {
303
var next = this._nextCallback;
304
this._nextCallback = null;
305
if (next) {
306
next();
307
}
308
}
309
310
return this;
311
},
312
313
_onload : function(r) {
314
this._destroy();
315
316
// It is permissible to send back a falsey redirect to force a page
317
// reload, so we need to take this branch if the key is present.
318
if (r && (typeof r.redirect != 'undefined')) {
319
// Before we redirect to file downloads, we close the dialog. These
320
// redirects aren't real navigation events so we end up stuck in the
321
// dialog otherwise.
322
if (r.close) {
323
this._pop();
324
}
325
326
// If we're redirecting, don't re-enable for controls.
327
this._keepControlsDisabled = true;
328
329
JX.$U(r.redirect).go();
330
} else if (r && r.dialog) {
331
this._push();
332
this._root = JX.$N(
333
'div',
334
{className: 'jx-client-dialog'},
335
JX.$H(r.dialog));
336
JX.DOM.listen(
337
this._root,
338
'click',
339
[['jx-workflow-button'], ['tag:button']],
340
JX.Workflow._onbutton);
341
JX.DOM.listen(
342
this._root,
343
'didSyntheticSubmit',
344
[],
345
JX.Workflow._onsyntheticsubmit);
346
347
var onlink = JX.Workflow._onlink;
348
JX.DOM.listen(this._root, 'click', 'tag:a', onlink);
349
350
JX.DOM.listen(
351
this._root,
352
'mousedown',
353
'jx-dialog-resize',
354
JX.Workflow._onresizestart);
355
356
// Note that even in the presence of a content frame, we're doing
357
// everything here at top level: dialogs are fully modal and cover
358
// the entire window.
359
360
document.body.appendChild(this._root);
361
362
var d = JX.Vector.getDim(this._root);
363
var v = JX.Vector.getViewport();
364
var s = JX.Vector.getScroll();
365
366
// Normally, we position dialogs 100px from the top of the screen.
367
// Use more space if the dialog is large (at least roughly the size
368
// of the viewport).
369
var offset = Math.min(Math.max(20, (v.y - d.y) / 2), 100);
370
JX.$V(0, s.y + offset).setPos(this._root);
371
372
try {
373
JX.DOM.focus(JX.DOM.find(this._root, 'button', '__default__'));
374
var inputs = JX.DOM.scry(this._root, 'input')
375
.concat(JX.DOM.scry(this._root, 'textarea'));
376
var miny = Number.POSITIVE_INFINITY;
377
var target = null;
378
for (var ii = 0; ii < inputs.length; ++ii) {
379
if (inputs[ii].type != 'hidden') {
380
// Find the topleft-most displayed element.
381
var p = JX.$V(inputs[ii]);
382
if (p.y < miny) {
383
miny = p.y;
384
target = inputs[ii];
385
}
386
}
387
}
388
target && JX.DOM.focus(target);
389
} catch (_ignored) {}
390
391
// The `focus()` call may have scrolled the window. Scroll it back to
392
// where it was before -- we want to focus the control, but not adjust
393
// the scroll position.
394
395
// Dialogs are window-level, so scroll the window explicitly.
396
window.scrollTo(s.x, s.y);
397
398
} else if (this.getHandler()) {
399
this.getHandler()(r);
400
this._pop();
401
} else if (r) {
402
if (__DEV__) {
403
JX.$E('Response to workflow request went unhandled.');
404
}
405
}
406
},
407
_push : function() {
408
if (!this._pushed) {
409
this._pushed = true;
410
JX.Workflow._push(this);
411
}
412
},
413
_pop : function() {
414
if (this._pushed) {
415
this._pushed = false;
416
JX.Workflow._pop();
417
}
418
},
419
_destroy : function() {
420
if (this._root) {
421
JX.DOM.remove(this._root);
422
this._root = null;
423
}
424
},
425
426
start : function() {
427
var next = JX.bind(this, this._send);
428
429
this.pause();
430
this._nextCallback = next;
431
432
this.invoke('start', this);
433
434
this.resume();
435
},
436
437
_send: function() {
438
var uri = this.getURI();
439
var method = this.getMethod();
440
var r = new JX.Request(uri, JX.bind(this, this._onload));
441
var list_of_pairs = this._data;
442
list_of_pairs.push(['__wflow__', true]);
443
r.setDataWithListOfPairs(list_of_pairs);
444
r.setDataSerializer(this.getDataSerializer());
445
if (method) {
446
r.setMethod(method);
447
}
448
r.listen('finally', JX.bind(this, this.invoke, 'finally'));
449
r.listen('error', JX.bind(this, function(error) {
450
var e = this.invoke('error', error);
451
if (e.getStopped()) {
452
return;
453
}
454
// TODO: Default error behavior? On Facebook Lite, we just shipped the
455
// user to "/error/". We could emit a blanket 'workflow-failed' type
456
// event instead.
457
}));
458
r.send();
459
},
460
461
getRoutable: function() {
462
var routable = new JX.Routable();
463
routable.listen('start', JX.bind(this, function() {
464
// Pass the event to allow other listeners to "start" to configure this
465
// workflow before it fires.
466
JX.Stratcom.pass(JX.Stratcom.context());
467
this.start();
468
}));
469
this.listen('finally', JX.bind(routable, routable.done));
470
return routable;
471
},
472
473
setData : function(dictionary) {
474
this._data = [];
475
for (var k in dictionary) {
476
this._data.push([k, dictionary[k]]);
477
}
478
return this;
479
},
480
481
addData: function(key, value) {
482
this._data.push([key, value]);
483
return this;
484
},
485
486
setDataWithListOfPairs : function(list_of_pairs) {
487
this._data = list_of_pairs;
488
return this;
489
}
490
},
491
492
properties : {
493
handler : null,
494
closeHandler : null,
495
dataSerializer : null,
496
method : null,
497
URI : null
498
},
499
500
initialize : function() {
501
502
function close_dialog_when_user_presses_escape(e) {
503
if (e.getSpecialKey() != 'esc') {
504
// Some key other than escape.
505
return;
506
}
507
508
if (JX.Stratcom.pass()) {
509
// Something else swallowed the event.
510
return;
511
}
512
513
var active = JX.Workflow._getActiveWorkflow();
514
if (!active) {
515
// No active workflow.
516
return;
517
}
518
519
// Note: the cancel button is actually an <a /> tag.
520
var buttons = JX.DOM.scry(active._root, 'a', 'jx-workflow-button');
521
if (!buttons.length) {
522
// No buttons in the dialog.
523
return;
524
}
525
526
var cancel = null;
527
for (var ii = 0; ii < buttons.length; ii++) {
528
if (buttons[ii].name == '__cancel__') {
529
cancel = buttons[ii];
530
break;
531
}
532
}
533
534
if (!cancel) {
535
// No 'Cancel' button.
536
return;
537
}
538
539
JX.Workflow._pop();
540
e.prevent();
541
}
542
543
JX.Stratcom.listen('keydown', null, close_dialog_when_user_presses_escape);
544
545
JX.Stratcom.listen('mousemove', null, JX.Workflow._onmousemove);
546
JX.Stratcom.listen('mouseup', null, JX.Workflow._onmouseup);
547
}
548
549
});
550
551