Path: blob/master/webroot/rsrc/externals/javelin/lib/Workflow.js
12242 views
/**1* @requires javelin-stratcom2* javelin-request3* javelin-dom4* javelin-vector5* javelin-install6* javelin-util7* javelin-mask8* javelin-uri9* javelin-routable10* @provides javelin-workflow11* @javelin12*/1314JX.install('Workflow', {15construct : function(uri, data) {16if (__DEV__) {17if (!uri || uri == '#') {18JX.$E(19'new JX.Workflow(<?>, ...): '+20'bogus URI provided when creating workflow.');21}22}23this.setURI(uri);24this.setData(data || {});25},2627events : ['error', 'finally', 'submit', 'start'],2829statics : {30_stack : [],31newFromForm : function(form, data, keep_enabled) {32var pairs = JX.DOM.convertFormToListOfPairs(form);33for (var k in data) {34pairs.push([k, data[k]]);35}3637var inputs;38if (keep_enabled) {39inputs = [];40} else {41// Disable form elements during the request42inputs = [].concat(43JX.DOM.scry(form, 'input'),44JX.DOM.scry(form, 'button'),45JX.DOM.scry(form, 'textarea'));46for (var ii = 0; ii < inputs.length; ii++) {47if (inputs[ii].disabled) {48delete inputs[ii];49} else {50inputs[ii].disabled = true;51}52}53}5455var workflow = new JX.Workflow(form.getAttribute('action'), {});5657workflow._form = form;5859workflow.setDataWithListOfPairs(pairs);60workflow.setMethod(form.getAttribute('method'));6162var onfinally = JX.bind(workflow, function() {63if (!this._keepControlsDisabled) {64for (var ii = 0; ii < inputs.length; ii++) {65inputs[ii] && (inputs[ii].disabled = false);66}67}68});69workflow.listen('finally', onfinally);7071return workflow;72},73newFromLink : function(link) {74var workflow = new JX.Workflow(link.href);75return workflow;76},7778_push : function(workflow) {79JX.Mask.show();80JX.Workflow._stack.push(workflow);81},82_pop : function() {83var dialog = JX.Workflow._stack.pop();84(dialog.getCloseHandler() || JX.bag)();85dialog._destroy();86JX.Mask.hide();87},88_onlink: function(event) {89// See T13302. When a user clicks a link in a dialog and that link90// triggers a navigation event, we want to close the dialog as though91// they had pressed a button.9293// When Quicksand is enabled, this is particularly relevant because94// the dialog will stay in the foreground while the page content changes95// in the background if we do not dismiss the dialog.9697// If this is a Command-Click, the link will open in a new window.98var is_command = !!event.getRawEvent().metaKey;99if (is_command) {100return;101}102103var link = event.getNode('tag:a');104105// If the link is an anchor, or does not go anywhere, ignore the event.106var href = link.getAttribute('href');107if (typeof href !== 'string') {108return;109}110111if (!href.length || href[0] === '#') {112return;113}114115// This link will open in a new window.116if (link.target === '_blank') {117return;118}119120// This link is really a dialog button which we'll handle elsewhere.121if (JX.Stratcom.hasSigil(link, 'jx-workflow-button')) {122return;123}124125// Close the dialog.126JX.Workflow._pop();127},128_onbutton : function(event) {129130if (JX.Stratcom.pass()) {131return;132}133134// Get the button (which is sometimes actually another tag, like an <a />)135// which triggered the event. In particular, this makes sure we get the136// right node if there is a <button> with an <img /> inside it or137// or something similar.138var t = event.getNode('jx-workflow-button') ||139event.getNode('tag:button');140141// If this button disables workflow (normally, because it is a file142// download button) let the event through without modification.143if (JX.Stratcom.getData(t).disableWorkflow) {144return;145}146147event.prevent();148149if (t.name == '__cancel__' || t.name == '__close__') {150JX.Workflow._pop();151} else {152var form = event.getNode('jx-dialog');153JX.Workflow._dosubmit(form, t);154}155},156_onsyntheticsubmit : function(e) {157if (JX.Stratcom.pass()) {158return;159}160e.prevent();161var form = e.getNode('jx-dialog');162var button = JX.DOM.find(form, 'button', '__default__');163JX.Workflow._dosubmit(form, button);164},165_dosubmit : function(form, button) {166// Issue a DOM event first, so form-oriented handlers can act.167var dom_event = JX.DOM.invoke(form, 'didWorkflowSubmit');168if (dom_event.getPrevented()) {169return;170}171172var data = JX.DOM.convertFormToListOfPairs(form);173data.push([button.name, button.value || true]);174175var active = JX.Workflow._getActiveWorkflow();176177active._form = form;178179var e = active.invoke('submit', {form: form, data: data});180if (!e.getStopped()) {181// NOTE: Don't remove the current dialog yet because additional182// handlers may still want to access the nodes.183184// Disable whatever button the user clicked to prevent duplicate185// submission mistakes when you accidentally click a button multiple186// times. See T11145.187button.disabled = true;188189active190.setURI(form.getAttribute('action') || active.getURI())191.setDataWithListOfPairs(data)192.start();193}194},195_getActiveWorkflow : function() {196var stack = JX.Workflow._stack;197return stack[stack.length - 1];198},199200_onresizestart: function(e) {201var self = JX.Workflow;202if (self._resizing) {203return;204}205206var workflow = self._getActiveWorkflow();207if (!workflow) {208return;209}210211e.kill();212213var form = JX.DOM.find(workflow._root, 'div', 'jx-dialog');214var resize = e.getNodeData('jx-dialog-resize');215var node_y = JX.$(resize.resizeY);216217var dim = JX.Vector.getDim(form);218dim.y = JX.Vector.getDim(node_y).y;219220if (!form._minimumSize) {221form._minimumSize = dim;222}223224self._resizing = {225min: form._minimumSize,226form: form,227startPos: JX.$V(e),228startDim: dim,229resizeY: node_y,230resizeX: resize.resizeX231};232},233234_onmousemove: function(e) {235var self = JX.Workflow;236if (!self._resizing) {237return;238}239240var spec = self._resizing;241var form = spec.form;242var min = spec.min;243244var delta = JX.$V(e).add(-spec.startPos.x, -spec.startPos.y);245var src_dim = spec.startDim;246var dst_dim = JX.$V(src_dim.x + delta.x, src_dim.y + delta.y);247248if (dst_dim.x < min.x) {249dst_dim.x = min.x;250}251252if (dst_dim.y < min.y) {253dst_dim.y = min.y;254}255256if (spec.resizeX) {257JX.$V(dst_dim.x, null).setDim(form);258}259260if (spec.resizeY) {261JX.$V(null, dst_dim.y).setDim(spec.resizeY);262}263},264265_onmouseup: function() {266var self = JX.Workflow;267if (!self._resizing) {268return;269}270271self._resizing = false;272}273},274275members : {276_root : null,277_pushed : false,278_data : null,279280_form: null,281_paused: 0,282_nextCallback: null,283_keepControlsDisabled: false,284285getSourceForm: function() {286return this._form;287},288289pause: function() {290this._paused++;291return this;292},293294resume: function() {295if (!this._paused) {296JX.$E('Resuming a workflow which is not paused!');297}298299this._paused--;300301if (!this._paused) {302var next = this._nextCallback;303this._nextCallback = null;304if (next) {305next();306}307}308309return this;310},311312_onload : function(r) {313this._destroy();314315// It is permissible to send back a falsey redirect to force a page316// reload, so we need to take this branch if the key is present.317if (r && (typeof r.redirect != 'undefined')) {318// Before we redirect to file downloads, we close the dialog. These319// redirects aren't real navigation events so we end up stuck in the320// dialog otherwise.321if (r.close) {322this._pop();323}324325// If we're redirecting, don't re-enable for controls.326this._keepControlsDisabled = true;327328JX.$U(r.redirect).go();329} else if (r && r.dialog) {330this._push();331this._root = JX.$N(332'div',333{className: 'jx-client-dialog'},334JX.$H(r.dialog));335JX.DOM.listen(336this._root,337'click',338[['jx-workflow-button'], ['tag:button']],339JX.Workflow._onbutton);340JX.DOM.listen(341this._root,342'didSyntheticSubmit',343[],344JX.Workflow._onsyntheticsubmit);345346var onlink = JX.Workflow._onlink;347JX.DOM.listen(this._root, 'click', 'tag:a', onlink);348349JX.DOM.listen(350this._root,351'mousedown',352'jx-dialog-resize',353JX.Workflow._onresizestart);354355// Note that even in the presence of a content frame, we're doing356// everything here at top level: dialogs are fully modal and cover357// the entire window.358359document.body.appendChild(this._root);360361var d = JX.Vector.getDim(this._root);362var v = JX.Vector.getViewport();363var s = JX.Vector.getScroll();364365// Normally, we position dialogs 100px from the top of the screen.366// Use more space if the dialog is large (at least roughly the size367// of the viewport).368var offset = Math.min(Math.max(20, (v.y - d.y) / 2), 100);369JX.$V(0, s.y + offset).setPos(this._root);370371try {372JX.DOM.focus(JX.DOM.find(this._root, 'button', '__default__'));373var inputs = JX.DOM.scry(this._root, 'input')374.concat(JX.DOM.scry(this._root, 'textarea'));375var miny = Number.POSITIVE_INFINITY;376var target = null;377for (var ii = 0; ii < inputs.length; ++ii) {378if (inputs[ii].type != 'hidden') {379// Find the topleft-most displayed element.380var p = JX.$V(inputs[ii]);381if (p.y < miny) {382miny = p.y;383target = inputs[ii];384}385}386}387target && JX.DOM.focus(target);388} catch (_ignored) {}389390// The `focus()` call may have scrolled the window. Scroll it back to391// where it was before -- we want to focus the control, but not adjust392// the scroll position.393394// Dialogs are window-level, so scroll the window explicitly.395window.scrollTo(s.x, s.y);396397} else if (this.getHandler()) {398this.getHandler()(r);399this._pop();400} else if (r) {401if (__DEV__) {402JX.$E('Response to workflow request went unhandled.');403}404}405},406_push : function() {407if (!this._pushed) {408this._pushed = true;409JX.Workflow._push(this);410}411},412_pop : function() {413if (this._pushed) {414this._pushed = false;415JX.Workflow._pop();416}417},418_destroy : function() {419if (this._root) {420JX.DOM.remove(this._root);421this._root = null;422}423},424425start : function() {426var next = JX.bind(this, this._send);427428this.pause();429this._nextCallback = next;430431this.invoke('start', this);432433this.resume();434},435436_send: function() {437var uri = this.getURI();438var method = this.getMethod();439var r = new JX.Request(uri, JX.bind(this, this._onload));440var list_of_pairs = this._data;441list_of_pairs.push(['__wflow__', true]);442r.setDataWithListOfPairs(list_of_pairs);443r.setDataSerializer(this.getDataSerializer());444if (method) {445r.setMethod(method);446}447r.listen('finally', JX.bind(this, this.invoke, 'finally'));448r.listen('error', JX.bind(this, function(error) {449var e = this.invoke('error', error);450if (e.getStopped()) {451return;452}453// TODO: Default error behavior? On Facebook Lite, we just shipped the454// user to "/error/". We could emit a blanket 'workflow-failed' type455// event instead.456}));457r.send();458},459460getRoutable: function() {461var routable = new JX.Routable();462routable.listen('start', JX.bind(this, function() {463// Pass the event to allow other listeners to "start" to configure this464// workflow before it fires.465JX.Stratcom.pass(JX.Stratcom.context());466this.start();467}));468this.listen('finally', JX.bind(routable, routable.done));469return routable;470},471472setData : function(dictionary) {473this._data = [];474for (var k in dictionary) {475this._data.push([k, dictionary[k]]);476}477return this;478},479480addData: function(key, value) {481this._data.push([key, value]);482return this;483},484485setDataWithListOfPairs : function(list_of_pairs) {486this._data = list_of_pairs;487return this;488}489},490491properties : {492handler : null,493closeHandler : null,494dataSerializer : null,495method : null,496URI : null497},498499initialize : function() {500501function close_dialog_when_user_presses_escape(e) {502if (e.getSpecialKey() != 'esc') {503// Some key other than escape.504return;505}506507if (JX.Stratcom.pass()) {508// Something else swallowed the event.509return;510}511512var active = JX.Workflow._getActiveWorkflow();513if (!active) {514// No active workflow.515return;516}517518// Note: the cancel button is actually an <a /> tag.519var buttons = JX.DOM.scry(active._root, 'a', 'jx-workflow-button');520if (!buttons.length) {521// No buttons in the dialog.522return;523}524525var cancel = null;526for (var ii = 0; ii < buttons.length; ii++) {527if (buttons[ii].name == '__cancel__') {528cancel = buttons[ii];529break;530}531}532533if (!cancel) {534// No 'Cancel' button.535return;536}537538JX.Workflow._pop();539e.prevent();540}541542JX.Stratcom.listen('keydown', null, close_dialog_when_user_presses_escape);543544JX.Stratcom.listen('mousemove', null, JX.Workflow._onmousemove);545JX.Stratcom.listen('mouseup', null, JX.Workflow._onmouseup);546}547548});549550551