Compare commits

...

11 Commits

Author SHA1 Message Date
Alan Dipert
45621fdc8e Grunt 2018-10-08 21:20:05 -07:00
Alan Dipert
819446a5a5 Fix Grunt warning 2018-10-08 21:19:54 -07:00
Alan Dipert
a8c027e7c1 Grunt 2018-10-08 14:28:13 -07:00
Alan Dipert
87f16e3c3b Add comment explaining utility of _enableDraghover 2018-10-08 14:15:40 -07:00
Alan Dipert
d36c88dab4 Grunt 2018-10-05 22:43:07 -07:00
Alan Dipert
31cc704ee2 Simplify DnD for fileInputs, fix Firefox
- Simplified dragHover "plugin" by counting children instead of storing them
- Counting children fixes Firefox 57+ bug (to be found or filed) that causes
text object of input element to produce drag events
- Removed multimethod since it's no longer used anywhere
2018-10-05 22:40:39 -07:00
Alan Dipert
6660d5c716 Grunt 2018-10-04 14:50:01 -07:00
Alan Dipert
9f068e9bbe Fix use of jQuery namespaces and add state machine handlers for previously-unhandled
states

- jQuery event namespaces were erroneously used to make events more specific,
  when instead what we needed were unique custom event names. Namespaces usage
  has been fixed, and we only use them now for expedient de-registration.
- A set of new zone state machine transitions have been added and documented
  that prevent certain "DnD unhandled transition" console messages on Firefox.
2018-10-04 14:43:18 -07:00
Alan Dipert
336db8e5f5 Grunt 2018-10-04 09:58:33 -07:00
Alan Dipert
fa5b43bc85 Update NEWS.md 2018-10-04 09:58:20 -07:00
Alan Dipert
b3ed915db2 Fixes #2142 DnD file upload on Firefox 57+
- Firefox 57+ appears not to fire a change event when the `files` field is modified,
  which prevented uploads from occuring. This commit triggers a change event manually
  and doesn't impact the functioning of other browsers.
- Namespaced events were used improperly in existing code which resulted in "jank"
  on Firefox. Namespaces shouldn't have been attached to events generated by the browser.
- The "drop" and "dragleave" handlers are now separate. This fixes a problem
  on Firefox where the drop event wasn't reliably changing the state of the input
  so it no longer glowed.
2018-10-04 09:50:29 -07:00
7 changed files with 178 additions and 673 deletions

View File

@@ -41,6 +41,8 @@ shiny 1.1.0.9001
* Fixed [#2162](https://github.com/rstudio/shiny/issues/2162): `selectInput` was sending spurious duplicate values to the server when using backspace. Thanks, @sada1993! [#2187](https://github.com/rstudio/shiny/pull/2187)
* Fixed [#2142](https://github.com/rstudio/shiny/issues/2142): Dropping files on `fileInput`s stopped working on recent releases of Firefox. Thanks @dmenne for reporting! [#2203](https://github.com/rstudio/shiny/pull/2203)
### Documentation Updates
* Addressed [#1864](https://github.com/rstudio/shiny/issues/1864) by changing `optgroup` documentation to use `list` instead of `c`. ([#2084](https://github.com/rstudio/shiny/pull/2084))

View File

@@ -2,8 +2,6 @@
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
//---------------------------------------------------------------------
@@ -323,227 +321,6 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
if (op === "==") return diff === 0;else if (op === ">=") return diff >= 0;else if (op === ">") return diff > 0;else if (op === "<=") return diff <= 0;else if (op === "<") return diff < 0;else throw "Unknown operator: " + op;
};
// multimethod: Creates functions — "multimethods" — that are polymorphic on one
// or more of their arguments.
//
// Multimethods can take any number of arguments. Arguments are passed to an
// applicable function or "method", returning its result. By default, if no
// method was applicable, an exception is thrown.
//
// Methods are searched in the order that they were added, and the first
// applicable method found is the one used.
//
// A method is applicable when the "dispatch value" associated with it
// corresponds to the value returned by the dispatch function. The dispatch
// function defaults to the value of the first argument passed to the
// multimethod.
//
// The correspondence between the value returned by the dispatch function and
// any method's dispatch value is determined by the test function, which is
// user-definable and defaults to `equal` or deep equality.
//
// # Chainable Functions
//
// The function returned by `multimethod()` exposes functions as properties.
// These functions generally return the multimethod, and so can be chained.
//
// - dispatch([function newDispatch]): Sets the dispatch function. The dispatch
// function can take any number of arguments, but must return a dispatch
// value. The default dispatch function returns the first argument passed to
// the multimethod.
//
// - test([function newTest]): Sets the test function. The test function takes
// two arguments: the dispatch value produced by the dispatch function, and
// the dispatch value associated with some method. It must return a boolean
// indicating whether or not to select the method. The default test function
// is `equal`.
//
// - when(object dispatchVal, function method): Adds a new dispatch value/method
// combination.
//
// - whenAny(array<object> dispatchVals, function method): Like `when`, but
// associates the method with every dispatch value in the `dispatchVals`
// array.
//
// - else(function newDefaultMethod): Sets the default function. This function
// is invoked when no methods apply. If left unset, the multimethod will throw
// an exception when no methods are applicable.
//
// - clone(): Returns a new, functionally-equivalent multimethod. This is a way
// to extend an existing multimethod in a local context — such as inside a
// function — without modifying the original. NOTE: The array of methods is
// copied, but the dispatch values themselves are not.
//
// # Self-reference
//
// The multimethod function can be obtained inside its method bodies without
// referring to it by name.
//
// This makes it possible for one method to call another, or to pass the
// multimethod to other functions as a callback from within methods.
//
// The mechanism is: the multimethod itself is bound as `this` to methods when
// they are called. Since arrow functions cannot be bound to objects, **self-reference
// is only possible within methods created using the `function` keyword**.
//
// # Tail recursion
//
// A method can call itself in a way that will not overflow the stack by using
// `this.recur`.
//
// `this.recur` is a function available in methods created using `function`.
// When the return value of a call to `this.recur` is returned by a method, the
// arguments that were supplied to `this.recur` are used to call the
// multimethod.
//
// # Examples
//
// Handling events:
//
// var handle = multimethod()
// .dispatch(e => [e.target.tagName.toLowerCase(), e.type])
// .when(["h1", "click"], e => "you clicked on an h1")
// .when(["p", "mouseover"], e => "you moused over a p"})
// .else(e => {
// let tag = e.target.tagName.toLowerCase();
// return `you did ${e.type} to an ${tag}`;
// });
//
// $(document).on("click mouseover mouseup mousedown", e => console.log(handle(e)))
//
// Self-calls:
//
// var demoSelfCall = multimethod()
// .when(0, function(n) {
// this(1);
// })
// .when(1, function(n) {
// doSomething(this);
// })
// .when(2, _ => console.log("tada"));
//
// Using (abusing?) the test function:
//
// var fizzBuzz = multimethod()
// .test((x, divs) => divs.map(d => x % d === 0).every(Boolean))
// .when([3, 5], x => "FizzBuzz")
// .when([3], x => "Fizz")
// .when([5], x => "Buzz")
// .else(x => x);
//
// for(let i = 0; i <= 100; i++) console.log(fizzBuzz(i));
//
// Getting carried away with tail recursion:
//
// var factorial = multimethod()
// .when(0, () => 1)
// .when(1, (_, prod = 1) => prod)
// .else(function(n, prod = 1) {
// return this.recur(n-1, n*prod);
// });
//
// var fibonacci = multimethod()
// .when(0, (_, a = 0) => a)
// .else(function(n, a = 0, b = 1) {
// return this.recur(n-1, b, a+b);
// });
function multimethod() {
var dispatch = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function (firstArg) {
return firstArg;
};
var test = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : equal;
var defaultMethod = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
var methods = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
var trampolining = false;
function Sentinel(args) {
this.args = args;
}
function trampoline(f) {
return function () {
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
trampolining = true;
var ret = f.apply(invoke, args);
while (ret instanceof Sentinel) {
ret = f.apply(invoke, ret.args);
}trampolining = false;
return ret;
};
}
var invoke = trampoline(function () {
for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
var dispatchVal = dispatch.apply(null, args);
for (var i = 0; i < methods.length; i++) {
var _methods$i = _slicedToArray(methods[i], 2);
var methodVal = _methods$i[0];
var methodFn = _methods$i[1];
if (test(dispatchVal, methodVal)) {
return methodFn.apply(invoke, args);
}
}
if (defaultMethod) {
return defaultMethod.apply(invoke, args);
} else {
throw new Error("No method for dispatch value " + dispatchVal);
}
});
invoke.recur = function () {
for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
args[_key4] = arguments[_key4];
}
if (!trampolining) throw new Error("recur can only be called inside a method");
return new Sentinel(args);
};
invoke.dispatch = function (newDispatch) {
dispatch = newDispatch;
return invoke;
};
invoke.test = function (newTest) {
test = newTest;
return invoke;
};
invoke.when = function (dispatchVal, methodFn) {
methods = methods.concat([[dispatchVal, methodFn]]);
return invoke;
};
invoke.whenAny = function (dispatchVals, methodFn) {
return dispatchVals.reduce(function (self, val) {
return invoke.when(val, methodFn);
}, invoke);
};
invoke.else = function () {
var newDefaultMethod = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
defaultMethod = newDefaultMethod;
return invoke;
};
invoke.clone = function () {
return multimethod(dispatch, test, defaultMethod, methods.slice());
};
return invoke;
}
//---------------------------------------------------------------------
// Source file: ../srcjs/browser.js
@@ -6038,68 +5815,79 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// This will be used only when restoring a file from a saved state.
return 'shiny.file';
},
_getZone: function _getZone(el) {
_zoneOf: function _zoneOf(el) {
return $(el).closest("div.input-group");
},
// This implements draghoverstart/draghoverend events that occur once per
// selector, instead of once for every child the way native
// dragenter/dragleave do. Inspired by https://gist.github.com/meleyal/3794126
_enableDraghover: function _enableDraghover($el) {
var ns = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
// Create an empty jQuery collection. This is a set-like data structure that
// jQuery normally uses to contain the results of a selection.
var collection = $();
// Attach a dragenter handler to $el and all of its children. When the first
// child is entered, trigger a draghoverstart event.
$el.on("dragenter.dragHover", function (e) {
if (collection.length === 0) {
$el.trigger("draghoverstart" + ns, e.originalEvent);
}
// Every child that has fired dragenter is added to the collection.
// Addition is idempotent, which accounts for elements producing dragenter
// multiple times.
collection = collection.add(e.originalEvent.target);
});
// Attach dragleave and drop handlers to $el and its children. Whenever a
// child fires either of these events, remove it from the collection.
$el.on("dragleave.dragHover drop.dragHover", function (e) {
collection = collection.not(e.originalEvent.target);
// When the collection has no elements, all of the children have been
// removed, and produce draghoverend event.
if (collection.length === 0) {
$el.trigger("draghoverend" + ns, e.originalEvent);
// This function makes it possible to attach listeners to the dragenter,
// dragleave, and drop events of a single element with children. It's not
// intuitive to do directly because outer elements fire "dragleave" events
// both when the drag leaves the element and when the drag enters a child. To
// make it easier, we maintain a count of the elements being dragged across
// and trigger 3 new types of event:
//
// 1. draghover:enter - When a drag enters el and any of its children.
// 2. draghover:leave - When the drag leaves el and all of its children.
// 3. draghover:drop - When an item is dropped on el or any of its children.
_enableDraghover: function _enableDraghover(el) {
var $el = $(el),
childCounter = 0;
$el.on({
"dragenter.draghover": function dragenterDraghover(e) {
if (childCounter++ === 0) {
$el.trigger("draghover:enter", e);
}
},
"dragleave.draghover": function dragleaveDraghover(e) {
if (--childCounter === 0) {
$el.trigger("draghover:leave", e);
}
if (childCounter < 0) {
console.error("draghover childCounter is negative somehow");
}
},
"dragover.draghover": function dragoverDraghover(e) {
e.preventDefault();
},
"drop.draghover": function dropDraghover(e) {
childCounter = 0;
$el.trigger("draghover:drop", e);
e.preventDefault();
}
});
return $el;
},
_disableDraghover: function _disableDraghover($el) {
$el.off(".dragHover");
_disableDraghover: function _disableDraghover(el) {
return $(el).off(".draghover");
},
_ZoneClass: {
ACTIVE: "shiny-file-input-active",
OVER: "shiny-file-input-over"
},
_enableDocumentEvents: function _enableDocumentEvents() {
var $doc = $("html");
var _this2 = this;
this._enableDraghover($doc);
$doc.on({
"draghoverstart.fileDrag": function draghoverstartFileDrag(e) {
$fileInputs.trigger("showZone.fileDrag");
var $doc = $("html");
var _ZoneClass = this._ZoneClass;
var ACTIVE = _ZoneClass.ACTIVE;
var OVER = _ZoneClass.OVER;
this._enableDraghover($doc).on({
"draghover:enter.draghover": function draghoverEnterDraghover(e) {
_this2._zoneOf($fileInputs).addClass(ACTIVE);
},
"draghoverend.fileDrag": function draghoverendFileDrag(e) {
$fileInputs.trigger("hideZone.fileDrag");
"draghover:leave.draghover": function draghoverLeaveDraghover(e) {
_this2._zoneOf($fileInputs).removeClass(ACTIVE);
},
"dragover.fileDrag drop.fileDrag": function dragoverFileDragDropFileDrag(e) {
e.preventDefault();
"draghover:drop.draghover": function draghoverDropDraghover(e) {
_this2._zoneOf($fileInputs).removeClass(OVER).removeClass(ACTIVE);
}
});
},
_disableDocumentEvents: function _disableDocumentEvents() {
var $doc = $("html");
$doc.off(".fileDrag");
$doc.off(".draghover");
this._disableDraghover($doc);
},
_zoneEvents: ["showZone.fileDrag", "hideZone.fileDrag", "draghoverstart.zone", "draghoverend.zone", "drop"].join(" "),
_canSetFiles: function _canSetFiles(fileList) {
var testEl = document.createElement("input");
testEl.type = "file";
@@ -6127,10 +5915,13 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// (Chrome, Safari)
$el.val("");
el.files = e.originalEvent.dataTransfer.files;
// Recent versions of Firefox (57+, or "Quantum" and beyond) don't seem to
// automatically trigger a change event, so we trigger one manually here.
// On browsers that do trigger change, this operation appears to be
// idempotent, as el.files doesn't change between events.
$el.trigger("change");
}
},
_activeClass: "shiny-file-input-active",
_overClass: "shiny-file-input-over",
_isIE9: function _isIE9() {
try {
return window.navigator.userAgent.match(/MSIE 9\./) && true || false;
@@ -6139,9 +5930,9 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
}
},
subscribe: function subscribe(el, callback) {
var _this2 = this;
var _this3 = this;
var $el = $(el);
$(el).on("change.fileInputBinding", uploadFiles);
// Here we try to set up the necessary events for Drag and Drop ("DnD") on
// every browser except IE9. We specifically exclude IE9 because it's one
// browser that supports just enough of the functionality we need to be
@@ -6151,89 +5942,38 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// supported based on this highlighting would be incorrect.
if (!this._isIE9()) {
(function () {
var $zone = _this2._getZone(el),
getState = function getState() {
return $el.data("state");
},
setState = function setState(newState) {
return $el.data("state", newState);
},
transition = multimethod().dispatch(function (e) {
return [getState(), e.type];
}).when(["plain", "showZone"], function (e) {
$zone.removeClass(_this2._overClass);
$zone.addClass(_this2._activeClass);
setState("activated");
}).when(["activated", "hideZone"], function (e) {
$zone.removeClass(_this2._overClass);
$zone.removeClass(_this2._activeClass);
setState("plain");
}).when(["activated", "draghoverstart"], function (e) {
$zone.addClass(_this2._overClass);
$zone.removeClass(_this2._activeClass);
setState("over");
})
// A "drop" event always coincides with a "draghoverend" event. Since
// we handle all draghoverend events the same way, by clearing our
// over-style and reverting to "activated" state, we only need to
// worry about handling the file upload itself here.
.when(["over", "drop"], function (e) {
_this2._handleDrop(e, el);
// State change taken care of by ["over", "draghoverend"] handler.
}).when(["over", "draghoverend"], function (e) {
$zone.removeClass(_this2._overClass);
$zone.addClass(_this2._activeClass);
setState("activated");
})
// This next case happens when the window (like Finder) that a file is
// being dragged from occludes the browser window, and the dragged
// item first enters the page over a drop zone instead of entering
// through a none-zone element.
//
// The dragenter event that caused this draghoverstart to occur will
// bubble to the document, where it will cause a showZone event to be
// fired, and drop zones will activate and their states will
// transition to "activated".
//
// We schedule a function to be run *after* that happens, using
// setTimeout. The function we schedule will set the current element's
// state to "over", preparing us to deal with a subsequent
// "draghoverend".
.when(["plain", "draghoverstart"], function (e) {
window.setTimeout(function () {
$zone.addClass(_this2._overClass);
$zone.removeClass(_this2._activeClass);
setState("over");
}, 0);
}).else(function (e) {
console.log("fileInput DnD unhandled transition", getState(), e.type, e);
});
if ($fileInputs.length === 0) _this2._enableDocumentEvents();
setState("plain");
$zone.on(_this2._zoneEvents, transition);
if ($fileInputs.length === 0) _this3._enableDocumentEvents();
$fileInputs = $fileInputs.add(el);
_this2._enableDraghover($zone, ".zone");
var $zone = _this3._zoneOf(el);
var OVER = _this3._ZoneClass.OVER;
_this3._enableDraghover($zone).on({
"draghover:enter.draghover": function draghoverEnterDraghover(e) {
$zone.addClass(OVER);
},
"draghover:leave.draghover": function draghoverLeaveDraghover(e) {
$zone.removeClass(OVER);
// Prevent this event from bubbling to the document handler,
// which would deactivate all zones.
e.stopPropagation();
},
"draghover:drop.draghover": function draghoverDropDraghover(e, dropEvent) {
_this3._handleDrop(dropEvent, el);
}
});
})();
}
$el.on("change.fileInputBinding", uploadFiles);
},
unsubscribe: function unsubscribe(el) {
var $el = $(el),
$zone = this._getZone(el);
$zone = this._zoneOf(el);
$el.removeData("state");
$zone.removeClass(this._overClass);
$zone.removeClass(this._activeClass);
$zone.removeClass(this._ZoneClass.OVER).removeClass(this._ZoneClass.ACTIVE);
this._disableDraghover($zone);
// Clean up local event handlers.
$el.off(".fileInputBinding");
$zone.off(this._zoneEvents);
$zone.off(".draghover");
// Remove el from list of inputs and (maybe) clean up global event handlers.
$fileInputs = $fileInputs.not(el);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -309,72 +309,77 @@ $.extend(fileInputBinding, {
// This will be used only when restoring a file from a saved state.
return 'shiny.file';
},
_getZone: function(el) {
_zoneOf: function(el) {
return $(el).closest("div.input-group");
},
// This implements draghoverstart/draghoverend events that occur once per
// selector, instead of once for every child the way native
// dragenter/dragleave do. Inspired by https://gist.github.com/meleyal/3794126
_enableDraghover: function($el, ns = "") {
// Create an empty jQuery collection. This is a set-like data structure that
// jQuery normally uses to contain the results of a selection.
let collection = $();
// Attach a dragenter handler to $el and all of its children. When the first
// child is entered, trigger a draghoverstart event.
$el.on("dragenter.dragHover", e => {
if (collection.length === 0) {
$el.trigger("draghoverstart" + ns, e.originalEvent);
}
// Every child that has fired dragenter is added to the collection.
// Addition is idempotent, which accounts for elements producing dragenter
// multiple times.
collection = collection.add(e.originalEvent.target);
});
// Attach dragleave and drop handlers to $el and its children. Whenever a
// child fires either of these events, remove it from the collection.
$el.on("dragleave.dragHover drop.dragHover", e => {
collection = collection.not(e.originalEvent.target);
// When the collection has no elements, all of the children have been
// removed, and produce draghoverend event.
if (collection.length === 0) {
$el.trigger("draghoverend" + ns, e.originalEvent);
}
});
},
_disableDraghover: function($el) {
$el.off(".dragHover");
},
_enableDocumentEvents: function() {
let $doc = $("html");
this._enableDraghover($doc);
$doc.on({
"draghoverstart.fileDrag": e => {
$fileInputs.trigger("showZone.fileDrag");
// This function makes it possible to attach listeners to the dragenter,
// dragleave, and drop events of a single element with children. It's not
// intuitive to do directly because outer elements fire "dragleave" events
// both when the drag leaves the element and when the drag enters a child. To
// make it easier, we maintain a count of the elements being dragged across
// and trigger 3 new types of event:
//
// 1. draghover:enter - When a drag enters el and any of its children.
// 2. draghover:leave - When the drag leaves el and all of its children.
// 3. draghover:drop - When an item is dropped on el or any of its children.
_enableDraghover: function(el) {
let $el = $(el),
childCounter = 0;
$el.on({
"dragenter.draghover": e => {
if (childCounter++ === 0) {
$el.trigger("draghover:enter", e);
}
},
"draghoverend.fileDrag": e => {
$fileInputs.trigger("hideZone.fileDrag");
"dragleave.draghover": e => {
if (--childCounter === 0) {
$el.trigger("draghover:leave", e);
}
if (childCounter < 0) {
console.error("draghover childCounter is negative somehow");
}
},
"dragover.fileDrag drop.fileDrag": e => {
"dragover.draghover": e => {
e.preventDefault();
},
"drop.draghover": e => {
childCounter = 0;
$el.trigger("draghover:drop", e);
e.preventDefault();
}
});
return $el;
},
_disableDraghover: function(el) {
return $(el).off(".draghover");
},
_ZoneClass: {
ACTIVE: "shiny-file-input-active",
OVER: "shiny-file-input-over"
},
_enableDocumentEvents: function() {
let $doc = $("html"),
{ACTIVE, OVER} = this._ZoneClass;
this._enableDraghover($doc)
.on({
"draghover:enter.draghover": e => {
this._zoneOf($fileInputs).addClass(ACTIVE);
},
"draghover:leave.draghover": e => {
this._zoneOf($fileInputs).removeClass(ACTIVE);
},
"draghover:drop.draghover": e => {
this._zoneOf($fileInputs)
.removeClass(OVER)
.removeClass(ACTIVE);
}
});
},
_disableDocumentEvents: function() {
let $doc = $("html");
$doc.off(".fileDrag");
$doc.off(".draghover");
this._disableDraghover($doc);
},
_zoneEvents: [
"showZone.fileDrag",
"hideZone.fileDrag",
"draghoverstart.zone",
"draghoverend.zone",
"drop"
].join(" "),
_canSetFiles: function(fileList) {
var testEl = document.createElement("input");
testEl.type = "file";
@@ -402,10 +407,13 @@ $.extend(fileInputBinding, {
// (Chrome, Safari)
$el.val("");
el.files = e.originalEvent.dataTransfer.files;
// Recent versions of Firefox (57+, or "Quantum" and beyond) don't seem to
// automatically trigger a change event, so we trigger one manually here.
// On browsers that do trigger change, this operation appears to be
// idempotent, as el.files doesn't change between events.
$el.trigger("change");
}
},
_activeClass: "shiny-file-input-active",
_overClass: "shiny-file-input-over",
_isIE9: function() {
try {
return (window.navigator.userAgent.match(/MSIE 9\./) && true) || false;
@@ -414,7 +422,7 @@ $.extend(fileInputBinding, {
}
},
subscribe: function(el, callback) {
let $el = $(el);
$(el).on("change.fileInputBinding", uploadFiles);
// Here we try to set up the necessary events for Drag and Drop ("DnD") on
// every browser except IE9. We specifically exclude IE9 because it's one
// browser that supports just enough of the functionality we need to be
@@ -423,88 +431,39 @@ $.extend(fileInputBinding, {
// support the FileList object though, so the user's expectation that DnD is
// supported based on this highlighting would be incorrect.
if (!this._isIE9()) {
let $zone = this._getZone(el),
getState = () => $el.data("state"),
setState = (newState) => $el.data("state", newState),
transition = multimethod()
.dispatch(e => [getState(), e.type])
.when(["plain", "showZone"], e => {
$zone.removeClass(this._overClass);
$zone.addClass(this._activeClass);
setState("activated");
})
.when(["activated", "hideZone"], e => {
$zone.removeClass(this._overClass);
$zone.removeClass(this._activeClass);
setState("plain");
})
.when(["activated", "draghoverstart"], e => {
$zone.addClass(this._overClass);
$zone.removeClass(this._activeClass);
setState("over");
})
// A "drop" event always coincides with a "draghoverend" event. Since
// we handle all draghoverend events the same way, by clearing our
// over-style and reverting to "activated" state, we only need to
// worry about handling the file upload itself here.
.when(["over", "drop"], e => {
this._handleDrop(e, el);
// State change taken care of by ["over", "draghoverend"] handler.
})
.when(["over", "draghoverend"], e => {
$zone.removeClass(this._overClass);
$zone.addClass(this._activeClass);
setState("activated");
})
// This next case happens when the window (like Finder) that a file is
// being dragged from occludes the browser window, and the dragged
// item first enters the page over a drop zone instead of entering
// through a none-zone element.
//
// The dragenter event that caused this draghoverstart to occur will
// bubble to the document, where it will cause a showZone event to be
// fired, and drop zones will activate and their states will
// transition to "activated".
//
// We schedule a function to be run *after* that happens, using
// setTimeout. The function we schedule will set the current element's
// state to "over", preparing us to deal with a subsequent
// "draghoverend".
.when(["plain", "draghoverstart"], e => {
window.setTimeout(() => {
$zone.addClass(this._overClass);
$zone.removeClass(this._activeClass);
setState("over");
}, 0);
})
.else(e => {
console.log("fileInput DnD unhandled transition", getState(), e.type, e);
});
if ($fileInputs.length === 0) this._enableDocumentEvents();
setState("plain");
$zone.on(this._zoneEvents, transition);
$fileInputs = $fileInputs.add(el);
this._enableDraghover($zone, ".zone");
let $zone = this._zoneOf(el),
{OVER} = this._ZoneClass;
this._enableDraghover($zone)
.on({
"draghover:enter.draghover": e => {
$zone.addClass(OVER);
},
"draghover:leave.draghover": e => {
$zone.removeClass(OVER);
// Prevent this event from bubbling to the document handler,
// which would deactivate all zones.
e.stopPropagation();
},
"draghover:drop.draghover": (e, dropEvent) => {
this._handleDrop(dropEvent, el);
}
});
}
$el.on("change.fileInputBinding", uploadFiles);
},
unsubscribe: function(el) {
let $el = $(el),
$zone = this._getZone(el);
$zone = this._zoneOf(el);
$el.removeData("state");
$zone.removeClass(this._overClass);
$zone.removeClass(this._activeClass);
$zone
.removeClass(this._ZoneClass.OVER)
.removeClass(this._ZoneClass.ACTIVE);
this._disableDraghover($zone);
// Clean up local event handlers.
$el.off(".fileInputBinding");
$zone.off(this._zoneEvents);
$zone.off(".draghover");
// Remove el from list of inputs and (maybe) clean up global event handlers.
$fileInputs = $fileInputs.not(el);

View File

@@ -326,199 +326,3 @@ exports.compareVersion = function(a, op, b) {
else if (op === "<") return (diff < 0);
else throw `Unknown operator: ${op}`;
};
// multimethod: Creates functions — "multimethods" — that are polymorphic on one
// or more of their arguments.
//
// Multimethods can take any number of arguments. Arguments are passed to an
// applicable function or "method", returning its result. By default, if no
// method was applicable, an exception is thrown.
//
// Methods are searched in the order that they were added, and the first
// applicable method found is the one used.
//
// A method is applicable when the "dispatch value" associated with it
// corresponds to the value returned by the dispatch function. The dispatch
// function defaults to the value of the first argument passed to the
// multimethod.
//
// The correspondence between the value returned by the dispatch function and
// any method's dispatch value is determined by the test function, which is
// user-definable and defaults to `equal` or deep equality.
//
// # Chainable Functions
//
// The function returned by `multimethod()` exposes functions as properties.
// These functions generally return the multimethod, and so can be chained.
//
// - dispatch([function newDispatch]): Sets the dispatch function. The dispatch
// function can take any number of arguments, but must return a dispatch
// value. The default dispatch function returns the first argument passed to
// the multimethod.
//
// - test([function newTest]): Sets the test function. The test function takes
// two arguments: the dispatch value produced by the dispatch function, and
// the dispatch value associated with some method. It must return a boolean
// indicating whether or not to select the method. The default test function
// is `equal`.
//
// - when(object dispatchVal, function method): Adds a new dispatch value/method
// combination.
//
// - whenAny(array<object> dispatchVals, function method): Like `when`, but
// associates the method with every dispatch value in the `dispatchVals`
// array.
//
// - else(function newDefaultMethod): Sets the default function. This function
// is invoked when no methods apply. If left unset, the multimethod will throw
// an exception when no methods are applicable.
//
// - clone(): Returns a new, functionally-equivalent multimethod. This is a way
// to extend an existing multimethod in a local context — such as inside a
// function — without modifying the original. NOTE: The array of methods is
// copied, but the dispatch values themselves are not.
//
// # Self-reference
//
// The multimethod function can be obtained inside its method bodies without
// referring to it by name.
//
// This makes it possible for one method to call another, or to pass the
// multimethod to other functions as a callback from within methods.
//
// The mechanism is: the multimethod itself is bound as `this` to methods when
// they are called. Since arrow functions cannot be bound to objects, **self-reference
// is only possible within methods created using the `function` keyword**.
//
// # Tail recursion
//
// A method can call itself in a way that will not overflow the stack by using
// `this.recur`.
//
// `this.recur` is a function available in methods created using `function`.
// When the return value of a call to `this.recur` is returned by a method, the
// arguments that were supplied to `this.recur` are used to call the
// multimethod.
//
// # Examples
//
// Handling events:
//
// var handle = multimethod()
// .dispatch(e => [e.target.tagName.toLowerCase(), e.type])
// .when(["h1", "click"], e => "you clicked on an h1")
// .when(["p", "mouseover"], e => "you moused over a p"})
// .else(e => {
// let tag = e.target.tagName.toLowerCase();
// return `you did ${e.type} to an ${tag}`;
// });
//
// $(document).on("click mouseover mouseup mousedown", e => console.log(handle(e)))
//
// Self-calls:
//
// var demoSelfCall = multimethod()
// .when(0, function(n) {
// this(1);
// })
// .when(1, function(n) {
// doSomething(this);
// })
// .when(2, _ => console.log("tada"));
//
// Using (abusing?) the test function:
//
// var fizzBuzz = multimethod()
// .test((x, divs) => divs.map(d => x % d === 0).every(Boolean))
// .when([3, 5], x => "FizzBuzz")
// .when([3], x => "Fizz")
// .when([5], x => "Buzz")
// .else(x => x);
//
// for(let i = 0; i <= 100; i++) console.log(fizzBuzz(i));
//
// Getting carried away with tail recursion:
//
// var factorial = multimethod()
// .when(0, () => 1)
// .when(1, (_, prod = 1) => prod)
// .else(function(n, prod = 1) {
// return this.recur(n-1, n*prod);
// });
//
// var fibonacci = multimethod()
// .when(0, (_, a = 0) => a)
// .else(function(n, a = 0, b = 1) {
// return this.recur(n-1, b, a+b);
// });
function multimethod(dispatch = (firstArg) => firstArg,
test = equal,
defaultMethod = null,
methods = []) {
var trampolining = false;
function Sentinel (args) { this.args = args; }
function trampoline(f) {
return (...args) => {
trampolining = true;
var ret = f.apply(invoke, args);
while (ret instanceof Sentinel)
ret = f.apply(invoke, ret.args);
trampolining = false;
return ret;
};
}
let invoke = trampoline((...args) => {
var dispatchVal = dispatch.apply(null, args);
for (let i = 0; i < methods.length; i++) {
let [methodVal, methodFn] = methods[i];
if (test(dispatchVal, methodVal)) {
return methodFn.apply(invoke, args);
}
}
if (defaultMethod) {
return defaultMethod.apply(invoke, args);
} else {
throw new Error(`No method for dispatch value ${dispatchVal}`);
}
});
invoke.recur = (...args) => {
if (!trampolining) throw new Error("recur can only be called inside a method");
return new Sentinel(args);
};
invoke.dispatch = (newDispatch) => {
dispatch = newDispatch;
return invoke;
};
invoke.test = (newTest) => {
test = newTest;
return invoke;
};
invoke.when = (dispatchVal, methodFn) => {
methods = methods.concat([[dispatchVal, methodFn]]);
return invoke;
};
invoke.whenAny = (dispatchVals, methodFn) => {
return dispatchVals.reduce((self, val) => invoke.when(val, methodFn), invoke);
};
invoke.else = (newDefaultMethod = null) => {
defaultMethod = newDefaultMethod;
return invoke;
};
invoke.clone = () => {
return multimethod(dispatch, test, defaultMethod, methods.slice());
};
return invoke;
}