/**
* Copyright (C) 2005-2016 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* <p>This mixin provides functions that allow files to be uploaded by dragging and dropping them
* onto the widget. It also provides functions that control highlighting the widget when
* files are dragged over the widget.</p>
* <p><b>NOTE: Highlighting of items provided by this module is not supported for any version of Internet
* Explorer prior to version 10</b></p>
*
* @module alfresco/documentlibrary/_AlfDndDocumentUploadMixin
* @extends module:alfresco/core/Core
* @mixes module:alfresco/documentlibrary/_AlfDocumentListTopicMixin
* @mixes module:alfresco/core/PathUtils
* @author Dave Draper
*/
define(["dojo/_base/declare",
"alfresco/core/Core",
"alfresco/documentlibrary/_AlfDocumentListTopicMixin",
"alfresco/core/PathUtils",
"alfresco/core/topics",
"dojo/_base/lang",
"dojo/_base/array",
"dojo/mouse",
"dojo/on",
"dijit/registry",
"dojo/dom-class",
"dojo/dom-construct",
"dojo/dom-geometry",
"dojo/dom-style",
"dojo/dom",
"dojo/_base/window",
"dojo/has",
"dojo/_base/sniff",
"jquery",
"jqueryui"],
function(declare, AlfCore, _AlfDocumentListTopicMixin, PathUtils, topics, lang, array, mouse, on, registry, domClass,
domConstruct, domGeom, domStyle, dom, win, has, $) {
return declare([AlfCore, _AlfDocumentListTopicMixin, PathUtils], {
/**
* An array of the i18n files to use with this widget.
*
* @instance
* @type {object[]}
* @default [{i18nFile: "./i18n/_AlfDndDocumentUploadMixin.properties"}]
* @since 1.0.41
*/
i18nRequirements: [{i18nFile: "./i18n/_AlfDndDocumentUploadMixin.properties"}],
/**
* An array of the CSS files to use with this widget.
*
* @instance cssRequirements {Array}
* @type {object[]}
* @default [{cssFile:"./css/_AlfDndDocumentUploadMixin.css"}]
* @since 1.0.41
*/
cssRequirements: [{cssFile:"./css/_AlfDndDocumentUploadMixin.css"}],
/**
* Indicates whether drag and drop is enabled.
*
* @instance
* @type {boolean}
* @default
*/
dndUploadEnabled: false,
/**
* Indicates whether or not the browser is capable of drag and drop file upload. This is set in the constructor.
*
* @instance
* @type {boolean}
* @default
*/
dndUploadCapable: false,
/**
* Keeps track of the DOM node that the drag-and-drop events are listened on.
*
* @instance
* @type {object}
* @default
*/
dragAndDropNode: null,
/**
*
* @instance
* @type {element}
* @default
* @since 1.0.41
*/
dragAndDropOverlayNode: null,
/**
* @instance
* @type {object[]}
* @default
*/
dndUploadEventHandlers: null,
/**
* This is the length of time (in milliseconds) that the highlight will be displayed on the screen without the mouse
* moving over any element within the element on which the highlight can be applied. This exists so
* that if the drag moves out of the browser without leaving the element (i.e. if it is moved onto
* an overlapping window) the highlight will not remain displayed forever.
*
* @instance
* @type {number}
* @default
* @since 1.0.42
*/
dndUploadHighlightDebounceTime: 2500,
/**
* The image to use for the upload highlighting. Currently the only other option apart from the default is
* "elipse-cross.png"
*
* @instance
* @type {string}
* @default
* @since 1.0.41
*/
dndUploadHighlightImage: "large-folder-icon.png",
/**
* The text to display for upload highlighting. If configured or overridden to be null or the
* empty string then no text will be displayed.
*
* @instance
* @type {string}
* @default
* @since 1.0.41
*/
dndUploadHighlightText: "dnd.upload.highlight.label",
/**
* This is used as a reference for a timeout handle that will remove the highlight if the mouse
* does not move over an element within the element that the upload highlight can be applied to.
*
* @instance
* @type {number}
* @default
* @since 1.0.42
*/
dndUploadHighlightTimeout: null,
/**
* Maximum number of files to allow to be published from a single drag operation. As for some user-agents
* drag and drop of potentialy large nested folder structures may be supported - this limit will halt the
* publish of the file list if it is larger than the supplied value and throw an error. Specify zero to
* indicate no limit - which is the default setting to maintain backward compatibility.
*
* @instance
* @type {integer}
* @default
* @since 1.0.59
*/
dndMaxFileLimit: 0,
/**
* Indicates whether or not the mixing module should take advantage of the drag-and-drop uploading capabilities.
* This makes it possible to "opt-out" through configuration or extension.
*
* @instance
* @type {boolean}
* @default
* @since 1.0.39
*/
suppressDndUploading: false,
/**
* This is a custom event that is emitted when the drag-and-drop upload highlight is
* [applied]{@link module:alfresco/documentlibrary/_AlfDndDocumentUploadMixin#addDndHighlight} to the node
* and results in any outer elements removing any previously applied highlight. This ensures that only
* one highlight is displayed at a time.
*
* @instance
* @type {string}
* @default
* @since 1.0.58
*/
_removeHighlightEvent: "ALF_REMOVE_DND_UPLOAD_HIGHLIGHT",
/**
* Determines whether or not the browser supports drag and drop file upload.
*
* @instance
*/
constructor: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__constructor() {
this.dndUploadCapable = ("draggable" in document.createElement("span"));
},
/**
* Removes HTML5 drag and drop listeners from the supplied DOM node
*
* @instance
* @param {object} domNode The DOM node to remove drag and drop capabilities from
*/
removeUploadDragAndDrop: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__removeUploadDragAndDrop(/*jshint unused:false*/ domNode) {
this.alfLog("log", "Removing drag and drop upload handlers");
// Clean up any previously created event handlers...
if (this.dndUploadEventHandlers)
{
array.forEach(this.dndUploadEventHandlers, function(handle){ handle.remove(); });
}
this.dndUploadEventHandlers = [];
},
/**
* Adds subscriptions to topics providing information on the changes to the current node being represented. This
* has been primarily added to support widgets that change the displayed view.
*
* @instance
*/
subscribeToCurrentNodeChanges: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__subscribeToCurrentNodeChanges(domNode) {
if (this.dndUploadCapable && !this.suppressDndUploading)
{
// Handle updates to the metadata (this is required in order for the view to know what
// root Node it represents is)
this.dragAndDropNode = domNode;
this.alfSubscribe(this.metadataChangeTopic, lang.hitch(this, this.onCurrentNodeChange));
this.alfSubscribe(this.hashChangeTopic, lang.hitch(this, this.onFilterChange));
}
},
/**
* Handles changes to the current node that is represented by the widget that mixes in this module. For example
* when the path that a view is displaying changes.
*
* @instance
* @param {object} payload The published payload
*/
onCurrentNodeChange: function alfresco_documentlibrary___AlfDndDocumentUploadMixin__onCurrentNodeChange(payload) {
if (payload && payload.node)
{
this.alfLog("log", "Updating current nodeRef to: ", payload.node);
this._currentNode = payload.node;
// Check that the current user can create children on the current node to determine
// whether or not DND upload should be supported...
var canCreateChildren = lang.getObject("node.parent.permissions.user.CreateChildren", false, payload);
if (canCreateChildren === true)
{
this.addUploadDragAndDrop(this.dragAndDropNode);
}
else
{
this.removeUploadDragAndDrop(this.dragAndDropNode);
}
}
else
{
this.alfLog("error", "A request was made to update the current NodeRef, but no 'node' property was provided in the payload: ", payload);
}
},
/**
* Handles changes the current filter. If the filter isn't path based then drag and drop uploading is disabled.
*
* @instance
* @param {object} payload The published payload
*/
onFilterChange: function alfresco_documentlibrary___AlfDndDocumentUploadMixin__onFilterChange(payload) {
var path = lang.getObject("path", false, payload);
if (!path)
{
this.removeUploadDragAndDrop(this.dragAndDropNode);
}
else
{
this.addUploadDragAndDrop(this.dragAndDropNode);
}
},
/**
* This function is used to check whether the currentItem supports upload for the permissions
* held by the current user. By default this assumes that the currentItem is a Node that defines
* all the relevant permissions. It looks to see whether the Node is a container that the user
* can create a children on or is not a container that the user can write to.
*
* @instance
*/
hasUploadPermissions: function alfresco_documentlibrary___AlfDndDocumentUploadMixin__hasUploadPermissions() {
var isContainer = lang.getObject("currentItem.node.isContainer", false, this);
var userPermissions = lang.getObject("currentItem.node.permissions.user", false, this);
return userPermissions &&
((isContainer === true && userPermissions.CreateChildren === true) ||
(isContainer === false && userPermissions.Write === true));
},
/**
* Adds HTML5 drag and drop listeners to the supplied DOM node
*
* @instance
* @param {object} domNode The DOM node to add drag and drop listeners to
*/
addUploadDragAndDrop: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__addUploadDragAndDrop(domNode) {
if (this.dndUploadCapable && !this.suppressDndUploading)
{
this.alfLog("log", "Adding DND upload capabilities", this);
try
{
// Add listeners to the HTML5 drag and drop events
this.dndUploadEnabled = true;
this.dragAndDropNode = domNode;
// Adding the base class will set an invisible border that can then be "coloured" in when
// dragging and item over the node (this prevents the display "jumping")...
domClass.add(this.dragAndDropNode, "alfresco-documentlibrary-_AlfDndDocumentUploadMixin");
// Reset the handlers...
this.removeDndUploadHandlers();
this.addDndUploadHandlers();
}
catch(exception)
{
this.alfLog("error", "The following exception occurred adding Drag and Drop event handlers: ", exception);
}
}
else
{
this.alfLog("log", "Cannot add DND upload capabilities because the browser does not have the required capabilities", this);
}
},
/**
* Sets up the handlers for the drag and drop events. These handlers are all added to the
* [dndUploadEventHandlers]{@link module:alfresco/documentlibrary/_AlfDndDocumentUploadMixin#dndUploadEventHandlers}
* array so that they can be easily cleaned up by
* [removeDndUploadHandlers]{@link module:alfresco/documentlibrary/_AlfDndDocumentUploadMixin#removeDndUploadHandlers}
*
* @instance
* @since 1.0.41
*/
addDndUploadHandlers: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__removeDndUploadHandlers() {
this.dndUploadEventHandlers.push(on(win.body(), "dragenter", lang.hitch(this, this.onDndUploadDragEnter)));
this.dndUploadEventHandlers.push(on(this.dragAndDropNode, "dragover", lang.hitch(this, this.onDndUploadDragOver)));
this.dndUploadEventHandlers.push(on(this.dragAndDropNode, "drop", lang.hitch(this, this.onDndUploadDrop)));
// This detects the custom event bubbled by nested elements that have had the DND highlight applied. When this occurs
// the highlight should be removed from the current widget (because only one highlight should be displayed at a time)
on(this.domNode, this._removeHighlightEvent, lang.hitch(this, this.removeDndHighlight));
},
/**
*
*
* @instance
* @param {object} e HTML5 drag and drop event
*/
onDndUploadDragEnter: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__onDndUploadDragEnter(e) {
if (dom.isDescendant(e.target, this.dragAndDropNode) &&
this.checkApplicable(e.target, "onDndUploadDragEnter"))
{
this.addDndHighlight();
// We want to set a timeout for showing the highlight, if a timeout has previously been set we want
// to clear it, and then set a new timeout for this highlight. This is done in order to prevent the
// highlight remaining displayed when the drag event leaves the element by going onto an overlaid window
if (this.dndUploadHighlightTimeout)
{
clearTimeout(this.dndUploadHighlightTimeout);
}
this.dndUploadHighlightTimeout = setTimeout(lang.hitch(this, this.removeDndHighlight), this.dndUploadHighlightDebounceTime);
}
else
{
if (dom.isDescendant(e.target, this.dragAndDropOverlayNode))
{
this.alfLog("info", "Over the overlay!");
}
else
{
this.removeDndHighlight();
}
}
},
/**
* This function is used to check that the event to be handled relates directly to the current widget. This check is needed
* because it is possible that a widget that handles drag and drop could be a child of another widget that handles drag and
* drop.
*
* It returns true if the supplied DOM node belongs to the current widget (e.g. "this") and that the widget has the same
* function. This isn't a perfect solution as there is a possibility that another widget could have an identical function
* name but this should be unlikely. It would have been preferable to use the "isInstanceOf" function, but that would require
* a reference to the class that this function is being declared as part of!
*
* As long as the function stops the event then this should not be necessary.
*
* @instance
* @param {object} domNode The DOM node that the event has occurred on
* @param {string} currentFunctionName The name of the function being processed
* @returns true if the current function should be executed
*/
checkApplicable: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__checkApplicable(domNode, currentFunctionName) {
var applicable = false;
var widget = registry.getEnclosingWidget(domNode);
if (!widget)
{
// Something odd has happened. This should never really occur since in order for this function to be
// called a widget must have DnD capabilities added!
this.alfLog("log", "No widget found - unexpected behaviour: ", this);
}
else if (widget !== this &&
typeof widget[currentFunctionName] === "function" &&
widget.dndUploadEnabled === true)
{
// The event relates to a different widget
this.alfLog("debug", "Related drag enter detected: ", this.id);
}
else
{
// The event relates to the current instance
this.alfLog("debug", "Unrelated drag enter detected", this.id);
applicable = true;
}
return applicable;
},
/**
* Clean up any previously created event handlers stored in the
* [dndUploadEventHandlers]{@link module:alfresco/documentlibrary/_AlfDndDocumentUploadMixin#dndUploadEventHandlers}
* array.
*
* @instance
* @since 1.0.41
*/
removeDndUploadHandlers: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__removeDndUploadHandlers() {
this.dndUploadEnabled = false;
if (this.dndUploadEventHandlers)
{
array.forEach(this.dndUploadEventHandlers, function(handle){ handle.remove(); });
}
this.dndUploadEventHandlers = [];
},
/**
* It's important that the drag over event is handled and that "preventDefault" is called on it. If this is
* not done then the "drop" event will not be processed.
*
* @instance
* @param {object} e HTML5 drag and drop event
*/
onDndUploadDragOver: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__onDndUploadDragOver(e) {
e.stopPropagation();
e.preventDefault();
},
/**
* Fired when an object starts getting dragged. The event is swallowed because we only want to
* allow drag and drop events that begin outside the browser window (e.g. for files). This prevents
* users attempting to drag and drop the document and folder images as if they could re-arrange
* the document lib structure.
*
* @instance
* @param {object} e HTML5 drag and drop event
*/
swallowDragStart: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__swallowDragStart(e) {
e.stopPropagation();
e.preventDefault();
},
/**
* Fired when an object is dragged onto any node in the document body (unless the node has
* been explicitly overridden to invoke another function). Swallows the event.
*
* @instance
* @param {object} e HTML5 drag and drop event
*
*/
swallowDragEnter: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__swallowDragEnter(e) {
e.stopPropagation();
e.preventDefault();
},
/**
* Fired when an object is dragged over any node in the document body (unless the node has
* been explicitly overridden to invoke another function). Updates the drag behaviour to
* indicate that drops are not allowed and then swallows the event.
*
* @instance
* @param {object} e HTML5 drag and drop event
*/
swallowDragOver: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__swallowDragOver(e) {
e.dataTransfer.dropEffect = "none";
e.stopPropagation();
e.preventDefault();
},
/**
* Fired when an object is dropped onto any node in the document body (unless the node has
* been explicitly overridden to invoke another function). Swallows the event to prevent
* default browser behaviour (i.e. attempting to open the file).
*
* @instance
* @param e {object} HTML5 drag and drop event
*/
swallowDrop: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__swallowDrop(e) {
this.alfLog("log", "Swallowing drop");
e.stopPropagation();
e.preventDefault();
},
/**
* This should be overridden to add highlighting when an item is dragged over the target.
*
* @instance
*/
addDndHighlight: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__addDragEnterHighlight() {
if (this.dragAndDropNode)
{
// Create a new node for indicating that a drag and drop upload is possible.
if (!this.dragAndDropOverlayNode)
{
// NOTE: We are deliberately creating an svg element here in order to retain IE10 support.
// Firefox, Chrome and IE11 would support pointer-events none on any element, but IE10
// only supports this on SVG elements.
this.dragAndDropOverlayNode = domConstruct.create("svg", {
className: "alfresco-documentlibrary-_AlfDndDocumentUploadMixin__overlay"
}, win.body());
var pNode = domConstruct.create("p", {
className: "alfresco-documentlibrary-_AlfDndDocumentUploadMixin__overlay__info"
}, this.dragAndDropOverlayNode);
if (this.dndUploadHighlightText)
{
domConstruct.create("span", {
className: "alfresco-documentlibrary-_AlfDndDocumentUploadMixin__overlay__info__title",
innerHTML: this.message(this.dndUploadHighlightText)
}, pNode);
domConstruct.create("br", {}, pNode);
}
domConstruct.create("img", {
className: "alfresco-documentlibrary-_AlfDndDocumentUploadMixin__overlay__info__icon",
src: require.toUrl("alfresco/documentlibrary/css/images/" + this.dndUploadHighlightImage)
}, pNode);
}
this.setDndHighlightDimensions(this.dragAndDropNode);
domClass.add(this.dragAndDropNode, "alfresco-documentlibrary-_AlfDndDocumentUploadMixin--dnd-highlight");
domClass.add(this.dragAndDropOverlayNode, "alfresco-documentlibrary-_AlfDndDocumentUploadMixin__overlay--display");
// Emit a custom event that tells all outer elements to remove the DND highlight as only one highlight should
// be displayed at a time.
on.emit(this.domNode.parentNode, this._removeHighlightEvent, {
bubbles: true,
cancelable: true,
targetWidget: this
});
}
},
/**
* This sets the position and dimensions of the
* [dragAndDropOverlayNode]{@link module:alfresco/documentlibrary/_AlfDndDocumentUploadMixin#dragAndDropOverlayNode}
*
* @instance
* @param {object} [targetNode] An optional node to use instead of the
* [dragAndDropNode]{@link module:alfresco/documentlibrary/_AlfDndDocumentUploadMixin#dragAndDropNode}
* @overridable
* @since 1.0.42
*/
setDndHighlightDimensions: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__setDndHighlightDimensions(targetNode) {
// jshint maxstatements:false, maxcomplexity:false
// Backwards compatibility setup, as of 1.0.60, an argument wasn't passed and this.dragAndDropNode was always used,
// this ensures that this behaviour is retained...
if (!targetNode)
{
targetNode = this.dragAndDropNode;
}
var computedStyle = domStyle.getComputedStyle(targetNode);
var dndNodeDimensions = domGeom.getMarginBox(targetNode, computedStyle);
var dndNodePosition = domGeom.position(targetNode);
var top, height;
var scrollParent = this.findScrollParent(targetNode);
if (scrollParent.is("html"))
{
var clientHeight = $(window).height();
var howFarScrolled = $(window).scrollTop();
var heightOfDndNode = dndNodeDimensions.h;
var whereDoesDndNodeStart = $(targetNode).offset().top;
if (howFarScrolled >= whereDoesDndNodeStart)
{
// We've scrolled beyond the start of the node. Therefore we need to start below the of the node
if (howFarScrolled >= whereDoesDndNodeStart + heightOfDndNode)
{
// but we've scrolled past the end of the node so there will be nothing to display
top = 0;
height = 0;
}
else
{
// Set the top as the location scrolled to...
top = howFarScrolled;
// Now work out the height...
var overlayOffset = howFarScrolled - whereDoesDndNodeStart;
if (overlayOffset + clientHeight > heightOfDndNode)
{
// Using full client height would go beyond the height
height = heightOfDndNode - overlayOffset;
}
else
{
height = clientHeight;
}
}
}
else
{
// We haven't scrolled beyond the start of the node (in fact we might not have scrolled at all)...
// Initially set the top to be how far we've scrolled, but correct if it is before the start of the node
top = howFarScrolled;
if (top < whereDoesDndNodeStart)
{
top = whereDoesDndNodeStart;
}
// Work out the height based on the available space...
if (heightOfDndNode - top > clientHeight)
{
if (howFarScrolled === 0)
{
height = clientHeight - whereDoesDndNodeStart;
}
else
{
height = clientHeight;
}
}
else
{
height = heightOfDndNode;
}
}
}
else
{
// Get the positions of the scrollable node and the drag-and-drop node within the view port...
var dndNodeBoundingRect = targetNode.getBoundingClientRect();
var scrollAreaBoundingRect = $(scrollParent)[0].getBoundingClientRect();
// Now work out the appropriate position of the overlay...
if (dndNodeBoundingRect.top > scrollAreaBoundingRect.top)
{
// Top of drop target is below the top of scroll area
top = dndNodeBoundingRect.top;
if (dndNodeBoundingRect.bottom >= scrollAreaBoundingRect.bottom)
{
// ...the bottom of the drop target is BELOW that of the scroll area... this means
// that we just need to place the overlay over the ENTIRE scroll area...
height = scrollAreaBoundingRect.bottom - dndNodeBoundingRect.top;
}
else
{
// ...the bottom of the drop target is ABOVE that of the scroll area, this means
// that we shouldn't overlay all the way to the bottom of the scroll area...
height = dndNodeBoundingRect.bottom - dndNodeBoundingRect.top;
}
}
else
{
// Top of drop target is above the top of the scroll area (this implies that it has been scrolled)...
top = scrollAreaBoundingRect.top;
if (dndNodeBoundingRect.bottom >= scrollAreaBoundingRect.bottom)
{
// ...the bottom of the drop target is BELOW that of the scroll area... this means
// that we just need to place the overlay over the ENTIRE scroll area...
height = scrollAreaBoundingRect.bottom - scrollAreaBoundingRect.top;
}
else
{
// ...the bottom of the drop target is ABOVE that of the scroll area, this means
// that we shouldn't overlay all the way to the bottom of the scroll area...
height = dndNodeBoundingRect.bottom - scrollAreaBoundingRect.top;
}
}
}
domStyle.set(this.dragAndDropOverlayNode, {
height: height + "px",
width: dndNodeDimensions.w + "px",
top: top + "px",
left: dndNodePosition.x + "px"
});
},
/**
* This function recursively searches out through the DOM to find the first parent of the supplied element that
* is capable of scrolling vertically and has scrollbars displayed.
*
* @instance
* @returns {element} The DOM element that is the scroll parent with scroll bars displayed
* @since 1.0.60
*/
findScrollParent: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__findScrollParent(domNode) {
var scrollParent;
if (domNode === document)
{
// We've managed to get all the way to document, return html instead.
scrollParent = $("html");
}
else
{
scrollParent = $(domNode).scrollParent();
if (scrollParent.is("html"))
{
// No action required - just return "html" as the scroll parent
}
else if (scrollParent[0].clientHeight === scrollParent[0].scrollHeight)
{
// Find the next parent because, the current scroll parent is NOT making use of the scroll bar
scrollParent = this.findScrollParent(scrollParent[0]);
}
}
return scrollParent;
},
/**
* This should be overridden to remove highlighting when an item is dragged out of the target
*
* @instance
*/
removeDndHighlight: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__addDragEnterHighlight() {
if (this.dragAndDropNode)
{
domClass.remove(this.dragAndDropNode, "alfresco-documentlibrary-_AlfDndDocumentUploadMixin--dnd-highlight");
if (this.dragAndDropOverlayNode)
{
domClass.remove(this.dragAndDropOverlayNode, "alfresco-documentlibrary-_AlfDndDocumentUploadMixin__overlay--display");
}
}
},
/**
* Fired when an object is dropped onto the DocumentList DOM element.
* Checks that files are present for upload, determines the target (either the current document list or
* a specific folder rendered in the document list and then calls on the DNDUpload singleton component
* to perform the upload.
*
* @instance
* @param {object} evt HTML5 drag and drop event
* @fires module:alfresco/core/topics#UPLOAD_REQUEST
*/
onDndUploadDrop: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__onDndUploadDrop(evt) {
try
{
// Only perform a file upload if the user has *actually* dropped some files!
this.alfLog("log", "Upload drop detected", evt);
if (evt.dataTransfer.files !== undefined && evt.dataTransfer.files !== null && evt.dataTransfer.files.length > 0)
{
this.removeDndHighlight();
var destination = this._currentNode ? this._currentNode.nodeRef : null;
var config = this.getUploadConfig();
var defaultConfig = {
destination: destination,
siteId: null,
containerId: null,
uploadDirectory: null,
updateNodeRef: null,
description: "",
overwrite: false,
thumbnails: "doclib",
username: null
};
var updatedConfig = lang.mixin(defaultConfig, config);
var walkFileSystem = lang.hitch(this, function alfresco_documentlibrary__AlfDndDocumentUploadMixin__onDndUploadDrop__walkFileSystem(directory, callback, error) {
callback.limit = this.dndMaxFileLimit;
callback.pending = callback.pending || 0;
callback.files = callback.files || [];
// get a dir reader and cleanup file path
var reader = directory.createReader(),
relativePath = directory.fullPath.replace(/^\//, "");
var repeatReader = function alfresco_documentlibrary__AlfDndDocumentUploadMixin__onDndUploadDrop__walkFileSystem__repeatReader() {
// about to start an async callback function
callback.pending++;
reader.readEntries(function alfresco_documentlibrary__AlfDndDocumentUploadMixin__onDndUploadDrop__walkFileSystem__repeatReader__readEntries(entries) {
// processing an async callback function
callback.pending--;
array.forEach(entries, function(entry) {
if (entry.isFile)
{
// about to start an async callback function
callback.pending++;
entry.file(function(File) {
// add the relativePath property to each file - this can be used to rebuild the contents of
// a nested tree folder structure if an appropriate API is available to do so
File.relativePath = relativePath;
callback.files.push(File);
if (callback.limit && callback.files.length > callback.limit)
{
throw new Error("Maximum dnd file limit reached: " + callback.limit);
}
// processing an async callback function
if (--callback.pending === 0)
{
// fall out here if last item processed is a file entry
callback(callback.files);
}
}, error);
}
else
{
walkFileSystem(entry, callback, error);
}
});
// the reader API is a little esoteric,from the MDN docs:
// "Continue calling readEntries() until an empty array is returned.
// You have to do this because the API might not return all entries in a single call."
if (entries.length !== 0)
{
repeatReader();
}
// fall out here if last item processed is a dir entry e.g. empty dir
if (callback.pending === 0)
{
callback(callback.files);
}
}, error);
};
repeatReader();
});
var addSelectedFiles = lang.hitch(this, function alfresco_documentlibrary__AlfDndDocumentUploadMixin__onDndUploadDrop__addSelectedFiles(files) {
if (this.dndMaxFileLimit && files.length > this.dndMaxFileLimit)
{
throw new Error("Maximum dnd file limit reached: " + this.dndMaxFileLimit);
}
// Check to see whether or not the generated upload configuration indicates
// that an existing node will be created or not. If node is being updated then
// we need to generate an intermediary step to capture version and comments...
if (updatedConfig.overwrite === false)
{
// Set up a response topic for receiving notifications that the upload has completed...
var responseTopic = this.generateUuid();
this._uploadSubHandle = this.alfSubscribe(responseTopic, lang.hitch(this, this.onFileUploadComplete), true);
this.alfPublish(topics.UPLOAD_REQUEST, {
alfResponseTopic: responseTopic,
files: files,
targetData: updatedConfig
}, true);
}
else
{
// TODO: Check that only one file has been dropped and issue error...
this.publishUpdateRequest(updatedConfig, files);
}
});
var items = evt.dataTransfer.items || [], firstEntry;
// webkitGetAsEntry is a marker for determining FileSystem API support.
// SHA-2164 - Firefox claims support, but different impl. rather than code around differences, fallback.
if (items[0] && items[0].webkitGetAsEntry && !has("ff") && (firstEntry = items[0].webkitGetAsEntry()))
{
walkFileSystem(firstEntry.filesystem.root, function(files) {
addSelectedFiles(files);
}, function() {
// fallback to standard way if error happens
addSelectedFiles(evt.dataTransfer.files);
}
);
}
else
{
// fallback to standard way if no support for filesystem API
addSelectedFiles(evt.dataTransfer.files);
}
}
else
{
this.alfLog("error", "A drop event was detected, but no files were present for upload: ", evt.dataTransfer);
}
}
catch(exception)
{
this.alfLog("error", "The following error occurred when files were dropped onto the Document List: ", exception);
}
// Remove the drag highlight...
this.removeDndHighlight();
// Destroy the overlay node (required for views that will re-render all the contents)...
domConstruct.destroy(this.dragAndDropOverlayNode);
this.dragAndDropOverlayNode = null;
evt.stopPropagation();
evt.preventDefault();
},
/**
* This function publishes an update version request. It will request that a new dialog
* be displayed containing the form controls defined in
* [widgetsForUpdate]{@link module:alfresco/documentlibrary/_AlfDndDocumentUploadMixin#widgetsForUpdate}.
*
* @instance
* @param {object} uploadConfig
*
* @fires ALF_CREATE_FORM_DIALOG_REQUEST
*/
publishUpdateRequest: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__publishUpdateRequest(uploadConfig, files) {
// TODO: Work out the next minor and major increment versions...
// TODO: Localization required...
// Set up a response topic for receiving notifications that the upload has completed...
var responseTopic = this.generateUuid();
this._uploadSubHandle = this.alfSubscribe(responseTopic, lang.hitch(this, this.onFileUploadComplete), true);
// To avoid the issue with processing payloads containing files with native
// code in them, it is necessary to temporarily store the files in the data model...
var filesRef = this.generateUuid();
this.alfSetData(filesRef, files);
this.alfPublish("ALF_CREATE_FORM_DIALOG_REQUEST", {
dialogTitle: "Update",
dialogConfirmationButtonTitle: "Continue Update",
dialogCancellationButtonTitle: "Cancel",
formSubmissionTopic: topics.UPLOAD_REQUEST,
formSubmissionPayloadMixin: {
alfResponseTopic: responseTopic,
filesRefs: filesRef,
targetData: uploadConfig
},
fixedWidth: true,
widgets: lang.clone(this.widgetsForUpdate)
}, true);
},
/**
* This defines the form controls to include in an update version dialog that
* is displayed whenever a user attempts to drag and drop a new version onto
* an existing node.
*
* @instance
* @type {array}
*/
widgetsForUpdate: [
{
name: "alfresco/forms/controls/RadioButtons",
config: {
label: "This version has",
name: "targetData.majorVersion",
value: "false",
optionsConfig: {
fixed: [
{ label: "Minor changes", value: "false" },
{ label: "Major changes", value: "true" }
]
}
}
},
{
name: "alfresco/forms/controls/TinyMCE",
config: {
label: "Comments",
name: "targetData.description",
value: ""
}
}
],
/**
* This function makes a best guess at constructing upload configuration, but it can be overridden if required or if the attempt
* at configuration construction is not appropriate.
*
* When overriding the function should return an object with the following
* attributes:
* - uploadDirectoryName
* - destination (optional - required if siteId, containerId and uploadDirectory are not provided)
* - siteId (optional - required if destination is not provide)
* - containerId (optional - required if destination is not provide)
* - uploadDirectory (optional - required if destination is not provide)
*
*
* @instance
* @returns {object}
*/
getUploadConfig: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__getUploadConfig() {
var config = null;
if (this.currentItem &&
this.currentItem.jsNode &&
this.currentItem.jsNode.isContainer)
{
// Best guess configuration for a container node...
try
{
config = {
destination: this.currentItem.node.nodeRef
};
}
catch (e)
{
this.alfLog("warn", "Failed to generate upload configuration", e);
}
}
else if (this.currentItem &&
this.currentItem.jsNode &&
this.currentItem.jsNode.isContainer === false)
{
// Best guess configuration for a node...
try
{
config = {
updateNodeRef: this.currentItem.node.nodeRef,
overwrite: true,
majorVersion: false,
destination: null
};
}
catch (e)
{
this.alfLog("warn", "Failed to generate upload configuration", e);
}
}
else if (this._currentNode &&
this._currentNode.parent &&
this._currentNode.parent.nodeRef)
{
try
{
// Best guess for document list view...
config = {
destination: this._currentNode.parent.nodeRef
};
}
catch (e)
{
this.alfLog("warn", "Failed to generate upload configuration", e);
}
}
return config;
},
/**
* This function is called once the document upload is complete. It publishes a request to reload the
* current document list data.
*
* @instance
*/
onFileUploadComplete: function alfresco_documentlibrary__AlfDndDocumentUploadMixin__onFileUploadComplete() {
this.alfLog("log", "Upload complete");
this.alfUnsubscribeSaveHandles([this._uploadSubHandle]);
// Intentionally pass publishGlobal as false, global publication will still occur if publishToParent
// is true (and parentPubSubScope is global) or publishToParent is false and pubSubScope is global.
// This has been added as a work around specifically to address the issue of Thumbnail uploads that
// need to generate payloads and set scopes *before* clicks occur to maintain "open in new tab" support
this.alfPublish(this.reloadDataTopic, {}, false, this.publishToParent);
}
});
});