chonky
Version:
A File Browser component for React
1,426 lines (1,246 loc) • 185 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var styles = require('@material-ui/core/styles');
var merge = _interopDefault(require('deepmerge'));
var React = require('react');
var React__default = _interopDefault(React);
var reactDnd = require('react-dnd');
var reactDndHtml5Backend = require('react-dnd-html5-backend');
var reactIntl = require('react-intl');
var reactJss = require('react-jss');
var reactRedux = require('react-redux');
var shortid = _interopDefault(require('shortid'));
var toolkit = require('@reduxjs/toolkit');
var sort = _interopDefault(require('fast-sort'));
var FuzzySearch = _interopDefault(require('fuzzy-search'));
var watch = _interopDefault(require('redux-watch'));
var filesize = _interopDefault(require('filesize'));
var ExactTrie = _interopDefault(require('exact-trie'));
var useMediaQuery = _interopDefault(require('@material-ui/core/useMediaQuery'));
var classnames = _interopDefault(require('classnames'));
var Box = _interopDefault(require('@material-ui/core/Box'));
var ClickAwayListener = _interopDefault(require('@material-ui/core/ClickAwayListener'));
var hotkeys = _interopDefault(require('hotkeys-js'));
var Breadcrumbs = _interopDefault(require('@material-ui/core/Breadcrumbs'));
var Button = _interopDefault(require('@material-ui/core/Button'));
var Menu = _interopDefault(require('@material-ui/core/Menu'));
var ListItemIcon = _interopDefault(require('@material-ui/core/ListItemIcon'));
var ListItemText = _interopDefault(require('@material-ui/core/ListItemText'));
var MenuItem = _interopDefault(require('@material-ui/core/MenuItem'));
var Typography = _interopDefault(require('@material-ui/core/Typography'));
var InputAdornment = _interopDefault(require('@material-ui/core/InputAdornment'));
var TextField = _interopDefault(require('@material-ui/core/TextField'));
var AutoSizer = _interopDefault(require('react-virtualized-auto-sizer'));
var reactWindow = require('react-window');
var ListSubheader = _interopDefault(require('@material-ui/core/ListSubheader'));
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _createForOfIteratorHelperLoose(o, allowArrayLike) {
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
if (it) return (it = it.call(o)).next.bind(it);
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
return function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var useDebounce = function useDebounce(value, delay) {
var _useState = React.useState(value),
debouncedValue = _useState[0],
setDebouncedValue = _useState[1];
React.useEffect(function () {
var handler = setTimeout(function () {
setDebouncedValue(value);
}, delay);
return function () {
clearTimeout(handler);
};
}, [value, delay]);
return [debouncedValue, setDebouncedValue];
};
var UNINITIALIZED_SENTINEL = {};
var useStaticValue = function useStaticValue(factory) {
var valueRef = React.useRef(UNINITIALIZED_SENTINEL);
if (valueRef.current === UNINITIALIZED_SENTINEL) valueRef.current = factory();
return valueRef.current;
};
var useInstanceVariable = function useInstanceVariable(value) {
var ref = React.useRef(value);
React.useEffect(function () {
ref.current = value;
}, [ref, value]);
return ref;
};
var Logger = /*#__PURE__*/function () {
function Logger() {}
Logger.error = function error() {
var _console;
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
// eslint-disable-next-line no-console
(_console = console).error.apply(_console, ['[Chonky runtime error]'].concat(args));
};
Logger.warn = function warn() {
var _console2;
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
// eslint-disable-next-line no-console
(_console2 = console).warn.apply(_console2, ['[Chonky runtime warning]'].concat(args));
};
Logger.debug = function debug() {
var _console3;
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
// eslint-disable-next-line no-console
(_console3 = console).debug.apply(_console3, ['[Chonky runtime debug]'].concat(args));
};
Logger.formatBullets = function formatBullets(bullets) {
return "\n- " + bullets.join('\n- ');
};
return Logger;
}();
var FileHelper = /*#__PURE__*/function () {
function FileHelper() {}
FileHelper.isDirectory = function isDirectory(file) {
// Not a directory by default
return !!file && file.isDir === true;
};
FileHelper.isHidden = function isHidden(file) {
// Not hidden by default
return !!file && file.isHidden === true;
};
FileHelper.isSymlink = function isSymlink(file) {
// Not a symlink by default
return !!file && file.isSymlink === true;
};
FileHelper.isEncrypted = function isEncrypted(file) {
// Not encrypted by default
return !!file && file.isEncrypted === true;
};
FileHelper.isClickable = function isClickable(file) {
// Clickable by default
return !!file;
};
FileHelper.isOpenable = function isOpenable(file) {
// Openable by default
return !!file && file.openable !== false;
};
FileHelper.isSelectable = function isSelectable(file) {
// Selectable by default
return !!file && file.selectable !== false;
};
FileHelper.isDraggable = function isDraggable(file) {
// File & folders are draggable by default, `null` is not
return !!file && file.draggable !== false;
};
FileHelper.isDroppable = function isDroppable(file) {
// Folders are droppable by default, files are not
if (!file) return false;
if (file.isDir && file.droppable !== false) return true;
return file.droppable === true;
};
FileHelper.isDndOpenable = function isDndOpenable(file) {
// Folders are DnD openable by default, files are not
if (!FileHelper.isOpenable(file)) return false;
if (file.isDir && file.dndOpenable !== false) return true;
return file.dndOpenable === true;
};
FileHelper.getModDate = function getModDate(file) {
if (!file || file.modDate === null || file.modDate === undefined) return null;
return FileHelper.parseDate(file.modDate);
};
FileHelper.parseDate = function parseDate(maybeDate) {
if (typeof maybeDate === 'string' || typeof maybeDate === 'number') {
// We allow users to provide string and numerical representations of dates.
try {
return new Date(maybeDate);
} catch (error) {
Logger.error("Could not convert provided string/number into a date: " + error.message + " ", 'Invalid value:', maybeDate);
}
}
if (maybeDate instanceof Date && !isNaN(maybeDate.getTime())) {
// We only allow valid dates objects
return maybeDate;
} // If we have an invalid date representation, we just return null.
Logger.warn('Unsupported date representation:', maybeDate);
return null;
};
FileHelper.getChildrenCount = function getChildrenCount(file) {
if (!file || typeof file.childrenCount !== 'number') return null;
return file.childrenCount;
};
return FileHelper;
}();
var sanitizeInputArray = function sanitizeInputArray(mode, rawArray) {
var sanitizedFiles = [];
var errorMessages = [];
if ((mode === 'folderChain' || mode === 'fileActions') && !rawArray) ; else if (!Array.isArray(rawArray)) {
errorMessages.push("Expected \"" + mode + "\" prop to be an array, got \"" + typeof rawArray + "\" instead.");
} else {
var nonObjectFileCount = 0;
var missingFieldFileCount = 0;
var seenIds = new Set();
var duplicateIds = new Set();
for (var i = 0; i < rawArray.length; ++i) {
var item = rawArray[i];
if (!item) {
if (mode === 'fileActions') nonObjectFileCount++;else sanitizedFiles.push(null);
} else if (typeof item !== 'object') {
nonObjectFileCount++;
} else {
if (!item.id || mode !== 'fileActions' && !item.name) {
missingFieldFileCount++;
} else if (seenIds.has(item.id)) {
duplicateIds.add(item.id);
} else {
seenIds.add(item.id);
sanitizedFiles.push(item);
}
}
}
if (nonObjectFileCount) {
errorMessages.push("Detected " + nonObjectFileCount + " file(s) of invalid type. Remember " + "that \"files\" array should contain either objects or nulls.");
}
if (missingFieldFileCount) {
errorMessages.push("Detected " + missingFieldFileCount + " file(s) that are missing the " + "required fields. Remember that file object should define an " + "\"id\" and a \"name\".");
}
if (duplicateIds.size > 0) {
var repeatedIdsString = '"' + Array.from(duplicateIds).join('", "') + '"';
errorMessages.push("Detected " + duplicateIds.size + " file IDs that are used multiple " + "times. Remember that each file should have a unique IDs. The " + ("following IDs were seen multiple times: " + repeatedIdsString));
}
}
if (errorMessages.length > 0) {
var errorMessageString = '\n- ' + errorMessages.join('\n- ');
var arrayString;
var itemString;
if (mode === 'folderChain') {
arrayString = 'folder chain';
itemString = 'files';
} else if (mode === 'fileActions') {
arrayString = 'file actions';
itemString = 'file actions';
} else {
// mode === 'files'
arrayString = 'files';
itemString = 'files';
}
Logger.error("Errors were detected when sanitizing the " + arrayString + " array. " + ("Offending " + itemString + " were removed from the array. Summary of ") + ("validation errors: " + errorMessageString));
}
return {
sanitizedArray: sanitizedFiles,
errorMessages: errorMessages
};
};
/**
* We have option IDs in a separate file to avoid circular deps...
*/
var OptionIds = {
ShowHiddenFiles: 'show_hidden_files',
ShowFoldersFirst: 'show_folders_first',
DarkMode: 'dark_mode'
};
var SortOrder;
(function (SortOrder) {
SortOrder["ASC"] = "asc";
SortOrder["DESC"] = "desc";
})(SortOrder || (SortOrder = {}));
var selectInstanceId = function selectInstanceId(state) {
return state.instanceId;
};
var selectExternalFileActionHandler = function selectExternalFileActionHandler(state) {
return state.externalFileActionHandler;
};
var selectFileActionMap = function selectFileActionMap(state) {
return state.fileActionMap;
};
var selectFileActionIds = function selectFileActionIds(state) {
return state.fileActionIds;
};
var selectFileActionData = function selectFileActionData(fileActionId) {
return function (state) {
return selectFileActionMap(state)[fileActionId];
};
};
var selectToolbarItems = function selectToolbarItems(state) {
return state.toolbarItems;
};
var selectContextMenuItems = function selectContextMenuItems(state) {
return state.contextMenuItems;
};
var selectFolderChain = function selectFolderChain(state) {
return state.folderChain;
};
var selectCurrentFolder = function selectCurrentFolder(state) {
var folderChain = selectFolderChain(state);
var currentFolder = folderChain.length > 0 ? folderChain[folderChain.length - 1] : null;
return currentFolder;
};
var selectParentFolder = function selectParentFolder(state) {
var folderChain = selectFolderChain(state);
var parentFolder = folderChain.length > 1 ? folderChain[folderChain.length - 2] : null;
return parentFolder;
};
var selectRawFiles = function selectRawFiles(state) {
return state.rawFiles;
};
var selectFileMap = function selectFileMap(state) {
return state.fileMap;
};
var selectCleanFileIds = function selectCleanFileIds(state) {
return state.cleanFileIds;
};
var selectFileData = function selectFileData(fileId) {
return function (state) {
return fileId ? selectFileMap(state)[fileId] : null;
};
};
var selectHiddenFileIdMap = function selectHiddenFileIdMap(state) {
return state.hiddenFileIdMap;
};
var selectHiddenFileCount = function selectHiddenFileCount(state) {
return Object.keys(selectHiddenFileIdMap(state)).length;
};
var selectFocusSearchInput = function selectFocusSearchInput(state) {
return state.focusSearchInput;
};
var selectSearchString = function selectSearchString(state) {
return state.searchString;
};
var selectSelectionMap = function selectSelectionMap(state) {
return state.selectionMap;
};
var selectSelectedFileIds = function selectSelectedFileIds(state) {
return Object.keys(selectSelectionMap(state));
};
var selectSelectionSize = function selectSelectionSize(state) {
return selectSelectedFileIds(state).length;
};
var selectIsFileSelected = function selectIsFileSelected(fileId) {
return function (state) {
return !!fileId && !!selectSelectionMap(state)[fileId];
};
};
var selectSelectedFiles = function selectSelectedFiles(state) {
var fileMap = selectFileMap(state);
return Object.keys(selectSelectionMap(state)).map(function (id) {
return fileMap[id];
});
};
var selectSelectedFilesForAction = function selectSelectedFilesForAction(fileActionId) {
return function (state) {
var fileActionMap = state.fileActionMap;
var action = fileActionMap[fileActionId];
if (!action || !action.requiresSelection) return undefined;
return getSelectedFiles(state, action.fileFilter);
};
};
var selectSelectedFilesForActionCount = function selectSelectedFilesForActionCount(fileActionId) {
return function (state) {
var _getSelectedFilesForA;
return (_getSelectedFilesForA = getSelectedFilesForAction(state, fileActionId)) == null ? void 0 : _getSelectedFilesForA.length;
};
};
var selectDisableSelection = function selectDisableSelection(state) {
return state.disableSelection;
};
var selectFileViewConfig = function selectFileViewConfig(state) {
return state.fileViewConfig;
};
var selectSortActionId = function selectSortActionId(state) {
return state.sortActionId;
};
var selectSortOrder = function selectSortOrder(state) {
return state.sortOrder;
};
var selectOptionMap = function selectOptionMap(state) {
return state.optionMap;
};
var selectOptionValue = function selectOptionValue(optionId) {
return function (state) {
return selectOptionMap(state)[optionId];
};
};
var selectThumbnailGenerator = function selectThumbnailGenerator(state) {
return state.thumbnailGenerator;
};
var selectDoubleClickDelay = function selectDoubleClickDelay(state) {
return state.doubleClickDelay;
};
var selectIsDnDDisabled = function selectIsDnDDisabled(state) {
return state.disableDragAndDrop;
};
var selectClearSelectionOnOutsideClick = function selectClearSelectionOnOutsideClick(state) {
return state.clearSelectionOnOutsideClick;
};
var selectContextMenuMounted = function selectContextMenuMounted(state) {
return state.contextMenuMounted;
};
var selectContextMenuConfig = function selectContextMenuConfig(state) {
return state.contextMenuConfig;
};
var selectContextMenuTriggerFile = function selectContextMenuTriggerFile(state) {
var _fileMap$config$trigg;
var config = selectContextMenuConfig(state);
if (!config || !config.triggerFileId) return null;
var fileMap = selectFileMap(state);
return (_fileMap$config$trigg = fileMap[config.triggerFileId]) != null ? _fileMap$config$trigg : null;
}; // Raw selectors
var getFileActionMap = function getFileActionMap(state) {
return state.fileActionMap;
};
var getOptionMap = function getOptionMap(state) {
return state.optionMap;
};
var getFileMap = function getFileMap(state) {
return state.fileMap;
};
var getFileIds = function getFileIds(state) {
return state.fileIds;
};
var getCleanFileIds = function getCleanFileIds(state) {
return state.cleanFileIds;
};
var getSortActionId = function getSortActionId(state) {
return state.sortActionId;
};
var getSortOrder = function getSortOrder(state) {
return state.sortOrder;
};
var getSearchString = function getSearchString(state) {
return state.searchString;
};
var _getLastClick = function _getLastClick(state) {
return state.lastClick;
}; // Memoized selectors
var makeGetAction = function makeGetAction(fileActionSelector) {
return toolkit.createSelector([getFileActionMap, fileActionSelector], function (fileActionMap, fileActionId) {
return fileActionId && fileActionMap[fileActionId] ? fileActionMap[fileActionId] : null;
});
};
var makeGetOptionValue = function makeGetOptionValue(optionId, defaultValue) {
if (defaultValue === void 0) {
defaultValue = undefined;
}
return toolkit.createSelector([getOptionMap], function (optionMap) {
var value = optionMap[optionId];
if (value === undefined) {
return defaultValue;
}
return value;
});
};
var makeGetFiles = function makeGetFiles(fileIdsSelector) {
return toolkit.createSelector([getFileMap, fileIdsSelector], function (fileMap, fileIds) {
return fileIds.map(function (fileId) {
return fileId && fileMap[fileId] ? fileMap[fileId] : null;
});
});
};
var getSortedFileIds = /*#__PURE__*/toolkit.createSelector([getFileIds, getSortOrder, /*#__PURE__*/makeGetFiles(getFileIds), /*#__PURE__*/makeGetAction(getSortActionId), /*#__PURE__*/makeGetOptionValue(OptionIds.ShowFoldersFirst, false)], function (fileIds, sortOrder, files, sortAction, showFolderFirst) {
if (!sortAction) {
// We allow users to set the sort action ID to `null` if they want to use their
// own sorting mechanisms instead of relying on Chonky built-in sort.
return fileIds;
}
var prepareSortKeySelector = function prepareSortKeySelector(selector) {
return function (file) {
return selector(file);
};
};
var sortFunctions = [];
if (showFolderFirst) {
// If option is undefined (relevant actions is not enabled), we don't show
// folders first.
sortFunctions.push({
desc: prepareSortKeySelector(FileHelper.isDirectory)
});
}
if (sortAction.sortKeySelector) {
var _sortFunctions$push;
var configKeyName = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
sortFunctions.push((_sortFunctions$push = {}, _sortFunctions$push[configKeyName] = prepareSortKeySelector(sortAction.sortKeySelector), _sortFunctions$push));
}
if (sortFunctions.length === 0) return fileIds; // We copy the array because `fast-sort` mutates it
var sortedFileIds = sort([].concat(files)).by(sortFunctions).map(function (file) {
return file ? file.id : null;
});
return sortedFileIds;
});
var getSearcher = /*#__PURE__*/toolkit.createSelector([/*#__PURE__*/makeGetFiles(getCleanFileIds)], function (cleanFiles) {
return new FuzzySearch(cleanFiles, ['name'], {
caseSensitive: false
});
});
var getSearchFilteredFileIds = /*#__PURE__*/toolkit.createSelector([getCleanFileIds, getSearchString, getSearcher], function (cleanFileIds, searchString, searcher) {
return searchString ? searcher.search(searchString).map(function (f) {
return f.id;
}) : cleanFileIds;
});
var getHiddenFileIdMap = /*#__PURE__*/toolkit.createSelector([getSearchFilteredFileIds, /*#__PURE__*/makeGetFiles(getCleanFileIds), /*#__PURE__*/makeGetOptionValue(OptionIds.ShowHiddenFiles)], function (searchFilteredFileIds, cleanFiles, showHiddenFiles) {
var searchFilteredFileIdsSet = new Set(searchFilteredFileIds);
var hiddenFileIdMap = {};
cleanFiles.forEach(function (file) {
if (!file) return;else if (!searchFilteredFileIdsSet.has(file.id)) {
// Hidden by seach
hiddenFileIdMap[file.id] = true;
} else if (!showHiddenFiles && FileHelper.isHidden(file)) {
// Hidden by options
hiddenFileIdMap[file.id] = true;
}
});
return hiddenFileIdMap;
});
var getDisplayFileIds = /*#__PURE__*/toolkit.createSelector([getSortedFileIds, getHiddenFileIdMap],
/** Returns files that will actually be shown to the user. */
function (sortedFileIds, hiddenFileIdMap) {
return sortedFileIds.filter(function (id) {
return !id || !hiddenFileIdMap[id];
});
});
var getLastClickIndex = /*#__PURE__*/toolkit.createSelector([_getLastClick, getSortedFileIds],
/** Returns the last click index after ensuring it is actually still valid. */
function (lastClick, displayFileIds) {
if (!lastClick || lastClick.index > displayFileIds.length - 1 || lastClick.fileId != displayFileIds[lastClick.index]) {
return null;
}
return lastClick.index;
});
var selectors = {
// Raw selectors
getFileActionMap: getFileActionMap,
getOptionMap: getOptionMap,
getFileMap: getFileMap,
getFileIds: getFileIds,
getCleanFileIds: getCleanFileIds,
getSortActionId: getSortActionId,
getSortOrder: getSortOrder,
getSearchString: getSearchString,
_getLastClick: _getLastClick,
// Memoized selectors
getSortedFileIds: getSortedFileIds,
getSearcher: getSearcher,
getSearchFilteredFileIds: getSearchFilteredFileIds,
getHiddenFileIdMap: getHiddenFileIdMap,
getDisplayFileIds: getDisplayFileIds,
getLastClickIndex: getLastClickIndex,
// Parametrized selectors
makeGetAction: makeGetAction,
makeGetOptionValue: makeGetOptionValue,
makeGetFiles: makeGetFiles
}; // Selectors meant to be used outside of Redux code
var getFileData = function getFileData(state, fileId) {
return fileId ? selectFileMap(state)[fileId] : null;
};
var getIsFileSelected = function getIsFileSelected(state, file) {
// !!! We deliberately don't use `FileHelper.isSelectable` here as we want to
// reflect the state of Redux store accurately.
return !!selectSelectionMap(state)[file.id];
};
var getSelectedFiles = function getSelectedFiles(state) {
var fileMap = state.fileMap,
selectionMap = state.selectionMap;
var selectedFiles = Object.keys(selectionMap).map(function (id) {
return fileMap[id];
});
for (var _len = arguments.length, filters = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
filters[_key - 1] = arguments[_key];
}
var filteredSelectedFiles = filters.reduce(function (prevFiles, filter) {
return filter ? prevFiles.filter(filter) : prevFiles;
}, selectedFiles);
return filteredSelectedFiles;
};
var getSelectedFilesForAction = function getSelectedFilesForAction(state, fileActionId) {
return selectSelectedFilesForAction(fileActionId)(state);
};
/**
* Merges multiple file action arrays into one while removing duplicates
*/
var mergeFileActionsArrays = function mergeFileActionsArrays() {
var _Array;
var seenActionIds = new Set();
var addToSeen = function addToSeen(a) {
return !!seenActionIds.add(a.id);
};
var wasNotSeen = function wasNotSeen(a) {
return !seenActionIds.has(a.id);
};
for (var _len = arguments.length, fileActionArrays = new Array(_len), _key = 0; _key < _len; _key++) {
fileActionArrays[_key] = arguments[_key];
}
var duplicateFreeArrays = fileActionArrays.map(function (arr) {
var duplicateFreeArray = arr.filter(wasNotSeen);
duplicateFreeArray.map(addToSeen);
return duplicateFreeArray;
});
return (_Array = new Array()).concat.apply(_Array, duplicateFreeArrays);
};
var thunkUpdateRawFileActions = function thunkUpdateRawFileActions(rawFileActions, disableDefaultFileActions) {
return function (dispatch) {
var _sanitizeInputArray = sanitizeInputArray('fileActions', rawFileActions),
sanitizedArray = _sanitizeInputArray.sanitizedArray,
errorMessages = _sanitizeInputArray.errorMessages; // Add default actions unless user disabled them
var defaultActionsToAdd;
if (Array.isArray(disableDefaultFileActions)) {
var disabledActionIds = new Set(disableDefaultFileActions);
defaultActionsToAdd = DefaultFileActions.filter(function (action) {
return !disabledActionIds.has(action.id);
});
} else if (disableDefaultFileActions) {
defaultActionsToAdd = [];
} else {
defaultActionsToAdd = DefaultFileActions;
}
var fileActions = mergeFileActionsArrays(sanitizedArray, EssentialFileActions, defaultActionsToAdd);
var optionDefaults = {};
fileActions.map(function (a) {
return a.option ? optionDefaults[a.option.id] = a.option.defaultValue : null;
});
dispatch(reduxActions.setRawFileActions(rawFileActions));
dispatch(reduxActions.setFileActionsErrorMessages(errorMessages));
dispatch(reduxActions.setFileActions(fileActions));
dispatch(reduxActions.setOptionDefaults(optionDefaults));
dispatch(thunkUpdateToolbarNContextMenuItems(fileActions));
};
};
var thunkUpdateToolbarNContextMenuItems = function thunkUpdateToolbarNContextMenuItems(fileActions) {
return function (dispatch) {
var excludedToolbarFileActionIds = new Set([// TODO: Move decision to exclude actions somewhere else, as users' custom
// components might not give these actions special treatment like Chonky does.
ChonkyActions.OpenParentFolder.id]);
var toolbarItems = [];
var seenToolbarGroups = {};
var contextMenuItems = [];
var seenContextMenuGroups = {};
var getGroup = function getGroup(itemArray, seenMap, groupName) {
if (seenMap[groupName]) return seenMap[groupName];
var group = {
name: groupName,
fileActionIds: []
};
itemArray.push(group);
seenMap[groupName] = group;
return group;
};
for (var _iterator = _createForOfIteratorHelperLoose(fileActions), _step; !(_step = _iterator()).done;) {
var action = _step.value;
var button = action.button;
if (!button) continue;
if (button.toolbar && !excludedToolbarFileActionIds.has(action.id)) {
if (button.group) {
var group = getGroup(toolbarItems, seenToolbarGroups, button.group);
group.fileActionIds.push(action.id);
} else {
toolbarItems.push(action.id);
}
}
if (button.contextMenu) {
if (button.group) {
var _group = getGroup(contextMenuItems, seenContextMenuGroups, button.group);
_group.fileActionIds.push(action.id);
} else {
contextMenuItems.push(action.id);
}
}
}
dispatch(reduxActions.updateFileActionMenuItems([toolbarItems, contextMenuItems]));
};
};
var thunkUpdateDefaultFileViewActionId = function thunkUpdateDefaultFileViewActionId(fileActionId) {
return function (dispatch, getState) {
var _getState = getState(),
fileActionMap = _getState.fileActionMap;
var action = fileActionId ? fileActionMap[fileActionId] : null;
if (action && action.fileViewConfig) {
dispatch(reduxActions.setFileViewConfig(action.fileViewConfig));
}
};
};
var thunkActivateSortAction = function thunkActivateSortAction(fileActionId) {
return function (dispatch, getState) {
if (!fileActionId) return;
var _getState2 = getState(),
oldActionId = _getState2.sortActionId,
oldOrder = _getState2.sortOrder,
fileActionMap = _getState2.fileActionMap;
var action = fileActionMap[fileActionId];
if (!action || !action.sortKeySelector) return;
var order = oldOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
if (oldActionId !== fileActionId) {
order = SortOrder.ASC;
}
dispatch(reduxActions.setSort({
actionId: fileActionId,
order: order
}));
};
};
var thunkApplySelectionTransform = function thunkApplySelectionTransform(action) {
return function (dispatch, getState) {
var selectionTransform = action.selectionTransform;
if (!selectionTransform) return;
var state = getState();
var prevSelection = new Set(Object.keys(selectSelectionMap(state)));
var hiddenFileIds = new Set(Object.keys(selectHiddenFileIdMap(state)));
var newSelection = selectionTransform({
prevSelection: prevSelection,
fileIds: selectCleanFileIds(state),
fileMap: selectFileMap(state),
hiddenFileIds: hiddenFileIds
});
if (!newSelection) return;
if (newSelection.size === 0) {
dispatch(reduxActions.clearSelection());
} else {
dispatch(reduxActions.selectFiles({
fileIds: Array.from(newSelection),
reset: true
}));
}
};
};
/**
* Thunk that dispatches actions to the external (user-provided) action handler.
*/
var thunkDispatchFileAction = function thunkDispatchFileAction(data) {
return function (_dispatch, getState) {
Logger.debug("FILE ACTION DISPATCH: [" + data.id + "]", 'data:', data);
var state = getState();
var action = selectFileActionMap(state)[data.id];
var externalFileActionHandler = selectExternalFileActionHandler(state);
if (action) {
if (externalFileActionHandler) {
Promise.resolve(externalFileActionHandler(data))["catch"](function (error) {
return Logger.error("User-defined file action handler threw an error: " + error.message);
});
}
} else {
Logger.warn("Internal components dispatched the \"" + data.id + "\" file action, but such " + "action was not registered.");
}
};
};
/**
* Thunk that is used by internal components (and potentially the user) to "request"
* actions. When action is requested, Chonky "prepares" the action data by extracting it
* from Redux state. Once action data is ready, Chonky executes some side effect and/or
* dispatches the action to the external action handler.
*/
var thunkRequestFileAction = function thunkRequestFileAction(action, payload) {
return function (dispatch, getState) {
Logger.debug("FILE ACTION REQUEST: [" + action.id + "]", 'action:', action, 'payload:', payload);
var state = getState();
var instanceId = selectInstanceId(state);
if (!selectFileActionMap(state)[action.id]) {
Logger.warn("The action \"" + action.id + "\" was requested, but it is not registered. The " + "action will still be dispatched, but this might indicate a bug in " + "the code. Please register your actions by passing them to " + "\"fileActions\" prop.");
} // Determine files for the action if action requires selection
var selectedFiles = selectSelectedFiles(state);
var selectedFilesForAction = action.fileFilter ? selectedFiles.filter(action.fileFilter) : selectedFiles;
if (action.requiresSelection && selectedFilesForAction.length === 0) {
Logger.warn("Internal components requested the \"" + action.id + "\" file " + "action, but the selection for this action was empty. This " + "might a bug in the code of the presentational components.");
return;
}
var contextMenuTriggerFile = selectContextMenuTriggerFile(state);
var actionState = {
instanceId: instanceId,
selectedFiles: selectedFiles,
selectedFilesForAction: selectedFilesForAction,
contextMenuTriggerFile: contextMenuTriggerFile
}; // === Update sort state if necessary
var sortKeySelector = action.sortKeySelector;
if (sortKeySelector) dispatch(thunkActivateSortAction(action.id)); // === Update file view state if necessary
var fileViewConfig = action.fileViewConfig;
if (fileViewConfig) dispatch(reduxActions.setFileViewConfig(fileViewConfig)); // === Update option state if necessary
var option = action.option;
if (option) dispatch(reduxActions.toggleOption(option.id)); // === Apply selection transform if necessary
var selectionTransform = action.selectionTransform;
if (selectionTransform) dispatch(thunkApplySelectionTransform(action)); // Apply the effect
var effect = action.effect;
var maybeEffectPromise = undefined;
if (effect) {
try {
maybeEffectPromise = effect({
action: action,
payload: payload,
state: actionState,
reduxDispatch: dispatch,
getReduxState: getState
});
} catch (error) {
Logger.error("User-defined effect function for action " + action.id + " threw an " + ("error: " + error.message));
}
} // Dispatch the action to user code. Deliberately call it after all other
// operations are over.
return Promise.resolve(maybeEffectPromise).then(function (effectResult) {
var data = {
id: action.id,
action: action,
payload: payload,
state: actionState
};
triggerDispatchAfterEffect(dispatch, data, effectResult);
})["catch"](function (error) {
Logger.error("User-defined effect function for action " + action.id + " returned a " + ("promise that was rejected: " + error.message));
var data = {
id: action.id,
action: action,
payload: payload,
state: actionState
};
triggerDispatchAfterEffect(dispatch, data, undefined);
});
};
};
var triggerDispatchAfterEffect = function triggerDispatchAfterEffect(dispatch, data, effectResult) {
var preventDispatch = effectResult === true;
if (!preventDispatch) dispatch(thunkDispatchFileAction(data));
};
(function (FileViewMode) {
FileViewMode["List"] = "list";
FileViewMode["Compact"] = "compact";
FileViewMode["Grid"] = "grid";
})(exports.FileViewMode || (exports.FileViewMode = {}));
(function (ChonkyIconName) {
// Misc
ChonkyIconName["loading"] = "loading";
ChonkyIconName["dropdown"] = "dropdown";
ChonkyIconName["placeholder"] = "placeholder"; // File Actions: Drag & drop
ChonkyIconName["dndDragging"] = "dndDragging";
ChonkyIconName["dndCanDrop"] = "dndCanDrop";
ChonkyIconName["dndCannotDrop"] = "dndCannotDrop"; // File Actions: File operations
ChonkyIconName["openFiles"] = "openFiles";
ChonkyIconName["openParentFolder"] = "openParentFolder";
ChonkyIconName["copy"] = "copy";
ChonkyIconName["paste"] = "paste";
ChonkyIconName["share"] = "share";
ChonkyIconName["search"] = "search";
ChonkyIconName["selectAllFiles"] = "selectAllFiles";
ChonkyIconName["clearSelection"] = "clearSelection"; // File Actions: Sorting & options
ChonkyIconName["sortAsc"] = "sortAsc";
ChonkyIconName["sortDesc"] = "sortDesc";
ChonkyIconName["toggleOn"] = "toggleOn";
ChonkyIconName["toggleOff"] = "toggleOff"; // File Actions: File Views
ChonkyIconName["list"] = "list";
ChonkyIconName["compact"] = "compact";
ChonkyIconName["smallThumbnail"] = "smallThumbnail";
ChonkyIconName["largeThumbnail"] = "largeThumbnail"; // File Actions: Unsorted
ChonkyIconName["folder"] = "folder";
ChonkyIconName["folderCreate"] = "folderCreate";
ChonkyIconName["folderOpen"] = "folderOpen";
ChonkyIconName["folderChainSeparator"] = "folderChainSeparator";
ChonkyIconName["download"] = "download";
ChonkyIconName["upload"] = "upload";
ChonkyIconName["trash"] = "trash";
ChonkyIconName["fallbackIcon"] = "fallbackIcon"; // File modifiers
ChonkyIconName["symlink"] = "symlink";
ChonkyIconName["hidden"] = "hidden"; // Generic file types
ChonkyIconName["file"] = "file";
ChonkyIconName["license"] = "license";
ChonkyIconName["code"] = "code";
ChonkyIconName["config"] = "config";
ChonkyIconName["model"] = "model";
ChonkyIconName["database"] = "database";
ChonkyIconName["text"] = "text";
ChonkyIconName["archive"] = "archive";
ChonkyIconName["image"] = "image";
ChonkyIconName["video"] = "video";
ChonkyIconName["info"] = "info";
ChonkyIconName["key"] = "key";
ChonkyIconName["lock"] = "lock";
ChonkyIconName["music"] = "music";
ChonkyIconName["terminal"] = "terminal";
ChonkyIconName["users"] = "users"; // OS file types
ChonkyIconName["linux"] = "linux";
ChonkyIconName["ubuntu"] = "ubuntu";
ChonkyIconName["windows"] = "windows"; // Programming language file types
ChonkyIconName["rust"] = "rust";
ChonkyIconName["python"] = "python";
ChonkyIconName["nodejs"] = "nodejs";
ChonkyIconName["php"] = "php"; // Development tools file types
ChonkyIconName["git"] = "git"; // Brands file types
ChonkyIconName["adobe"] = "adobe"; // Other program file types
ChonkyIconName["pdf"] = "pdf";
ChonkyIconName["excel"] = "excel";
ChonkyIconName["word"] = "word";
ChonkyIconName["flash"] = "flash";
})(exports.ChonkyIconName || (exports.ChonkyIconName = {}));
var defineFileAction = function defineFileAction(action, effect) {
if (action.__payloadType !== undefined && (action.hotkeys || action.button)) {
var errorMessage = "Invalid definition was provided for file action \"" + action.id + "\". Actions " + "that specify hotkeys or buttons cannot define a payload type. If " + "your application requires this functionality, define two actions " + "and chain them using effects.";
Logger.error(errorMessage);
throw new Error(errorMessage);
}
action.effect = effect;
return action;
};
/**
* Recursively check the current element and the parent elements, going bottom-up.
* Returns the first element to match the predicate, otherwise returns null if such
* element is not found.
*/
var findElementAmongAncestors = function findElementAmongAncestors(maybeElement, predicate) {
if (!maybeElement) return maybeElement;
if (predicate(maybeElement)) return maybeElement;
if (maybeElement.parentElement) {
return findElementAmongAncestors(maybeElement.parentElement, predicate);
}
return null;
};
var elementIsInsideButton = function elementIsInsideButton(buttonCandidate) {
return !!findElementAmongAncestors(buttonCandidate, function (element) {
return element.tagName && element.tagName.toLowerCase() === 'button';
});
};
var getValueOrFallback = function getValueOrFallback(value, fallback, desiredType) {
if (desiredType) {
return typeof value === desiredType ? value : fallback;
}
return value !== undefined ? value : fallback;
};
var reduxThunks = {
selectRange: function selectRange(params) {
return function (dispatch, getState) {
var state = getState();
if (state.disableSelection) return;
var displayFileIds = selectors.getDisplayFileIds(state);
var fileIdsToSelect = displayFileIds.slice(params.rangeStart, params.rangeEnd + 1).filter(function (id) {
return id && FileHelper.isSelectable(state.fileMap[id]);
});
dispatch(reduxActions.selectFiles({
fileIds: fileIdsToSelect,
reset: !!params.reset
}));
};
}
};
var EssentialActions = {
/**
* Action that is dispatched when the user clicks on a file entry using their mouse.
* Both single clicks and double clicks trigger this action.
*/
MouseClickFile: /*#__PURE__*/defineFileAction({
id: 'mouse_click_file',
__payloadType: {}
}, function (_ref) {
var payload = _ref.payload,
reduxDispatch = _ref.reduxDispatch,
getReduxState = _ref.getReduxState;
if (payload.clickType === 'double') {
if (FileHelper.isOpenable(payload.file)) {
reduxDispatch(thunkRequestFileAction(ChonkyActions.OpenFiles, {
targetFile: payload.file,
// To simulate Windows Explorer and Nautilus behaviour,
// a double click on a file only opens that file even if
// there is a selection.
files: [payload.file]
}));
}
} else {
// We're dealing with a single click
var disableSelection = selectDisableSelection(getReduxState());
if (FileHelper.isSelectable(payload.file) && !disableSelection) {
if (payload.ctrlKey) {
// Multiple selection
reduxDispatch(reduxActions.toggleSelection({
fileId: payload.file.id,
exclusive: false
}));
reduxDispatch(reduxActions.setLastClickIndex({
index: payload.fileDisplayIndex,
fileId: payload.file.id
}));
} else if (payload.shiftKey) {
// Range selection
var lastClickIndex = selectors.getLastClickIndex(getReduxState());
if (typeof lastClickIndex === 'number') {
// We have the index of the previous click
var rangeStart = lastClickIndex;
var rangeEnd = payload.fileDisplayIndex;
if (rangeStart > rangeEnd) {
var _ref2 = [rangeEnd, rangeStart];
rangeStart = _ref2[0];
rangeEnd = _ref2[1];
}
reduxDispatch(reduxThunks.selectRange({
rangeStart: rangeStart,
rangeEnd: rangeEnd
}));
} else {
// Since we can't do a range selection, do a
// multiple selection
reduxDispatch(reduxActions.toggleSelection({
fileId: payload.file.id,
exclusive: false
}));
reduxDispatch(reduxActions.setLastClickIndex({
index: payload.fileDisplayIndex,
fileId: payload.file.id
}));
}
} else {
// Exclusive selection
reduxDispatch(reduxActions.toggleSelection({
fileId: payload.file.id,
exclusive: true
}));
reduxDispatch(reduxActions.setLastClickIndex({
index: payload.fileDisplayIndex,
fileId: payload.file.id
}));
}
} else {
if (!payload.ctrlKey && !disableSelection) {
reduxDispatch(reduxActions.clearSelection());
}
reduxDispatch(reduxActions.setLastClickIndex({
index: payload.fileDisplayIndex,
fileId: payload.file.id
}));
}
}
}),
/**
* Action that is dispatched when the user "clicks" on a file using their keyboard.
* Using Space and Enter keys counts as clicking.
*/
KeyboardClickFile: /*#__PURE__*/defineFileAction({
id: 'keyboard_click_file',
__payloadType: {}
}, function (_ref3) {
var payload = _ref3.payload,
reduxDispatch = _ref3.reduxDispatch,
getReduxState = _ref3.getReduxState;
reduxDispatch(reduxActions.setLastClickIndex({
index: payload.fileDisplayIndex,
fileId: payload.file.id
}));
if (payload.enterKey) {
// We only dispatch the Open Files action here when the selection is
// empty. Otherwise, `Enter` key presses are handled by the
// hotkey manager for the Open Files action.
if (selectSelectionSize(getReduxState()) === 0) {
reduxDispatch(thunkRequestFileAction(ChonkyActions.OpenFiles, {
targetFile: payload.file,
files: [payload.file]
}));
}
} else if (payload.spaceKey && FileHelper.isSelectable(payload.file)) {
reduxDispatch(reduxActions.toggleSelection({
fileId: payload.file.id,
exclusive: payload.ctrlKey
}));
}
}),
/**
* Action that is dispatched when user starts dragging some file.
*/
StartDragNDrop: /*#__PURE__*/defineFileAction({
id: 'start_drag_n_drop',
__payloadType: {}
}, function (_ref4) {
var payload = _ref4.payload,
reduxDispatch = _ref4.reduxDispatch,
getReduxState = _ref4.getReduxState;
var file = payload.draggedFile;
if (!getIsFileSelected(getReduxState(), file)) {
if (FileHelper.isSelectable(file)) {
reduxDispatch(reduxActions.selectFiles({
fileIds: [file.id],
reset: true
}));
}
}
}),
/**
* Action that is dispatched when user either cancels the drag & drop interaction,
* or drops a file somewhere.
*/
EndDragNDrop: /*#__PURE__*/defineFileAction({
id: 'end_drag_n_drop',
__payloadType: {}
}, function (_ref5) {
var payload = _ref5.payload,
reduxDispatch = _ref5.reduxDispatch,
getReduxState = _ref5.getReduxState;
if (getIsFileSelected(getReduxState(), payload.destination)) {
// Can't drop a selection into itself
return;
}
var draggedFile = payload.draggedFile,
selectedFiles = payload.selectedFiles;
var droppedFiles = selectedFiles.length > 0 ? selectedFiles : [draggedFile];
reduxDispatch(thunkRequestFileAction(ChonkyActions.MoveFiles, _extends({}, payload, {
files: droppedFiles
})));
}),
/**
* Action that is dispatched when user moves files from one folder to another,
* usually by dragging & dropping some files into the folder.
*/
MoveFiles: /*#__PURE__*/defineFileAction({
id: 'move_files',
__payloadType: {}
}),
/**
* Action that is dispatched when the selection changes for any reason.
*/
ChangeSelection: /*#__PURE__*/defineFileAction({
id: 'change_selection',
__payloadType: {}
}),
/**
* Action that is dispatched when user wants to open some files. This action is
* often triggered by other actions.
*/
OpenFiles: /*#__PURE__*/defineFileAction({
id: 'open_files',
__payloadType: {}
}),
/**
* Action that is triggered when user wants to go up a directory.
*/
OpenParentFolder: /*#__PURE__*/defineFileAction({
id: 'open_parent_folder',
hotkeys: ['backspace'],
button: {
name: 'Go up a directory',
toolbar: true,
contextMenu: false,
icon: exports.ChonkyIconName.openParentFolder,
iconOnly: true
}
}, function (_ref6) {
var reduxDispatch = _ref6.reduxDispatch,
getReduxState = _ref6.getReduxState;
var parentFolder = selectParentFolder(getReduxState());
if (FileHelper.isOpenable(parentFolder)) {
reduxDispatch(thunkRequestFileAction(ChonkyActions.OpenFiles, {
targetFile: parentFolder,
files: [parentFolder]
}));
} else {
Logger.warn('Open parent folder effect was triggered even though the parent folder' + ' is not openable. This indicates a bug in presentation components.');
}
}),
/**
* Action that is dispatched when user opens the context menu, either by right click
* on something or using the context menu button on their keyboard.
*/
OpenFileContextMenu: /*#__PURE__*/defineFileAction({
id: 'open_file_context_menu',
__payloadType: {}
}, function (_ref7) {
var payload = _ref7.payload,
reduxDispatch = _ref7.reduxDispatch,
getReduxState = _ref7.getReduxState;
// TODO: Check if the context menu component is actually enabled. There is a
// chance it doesn't matter if it is enabled or not - if it is not mounted,
// the action will simply have no effect. It also allows users to provide
// their own components - however, users could also flip the "context menu
// component mounted" switch...
var triggerFile = getFileData(getReduxState(), payload.triggerFileId);
if (triggerFile) {
var fileSelected = getIsFileSelected(getReduxState(), triggerFile);
if (!fileSelected) {
// If file is selected, we leave the selection as is. If it is not
// selected, it means user right clicked the file with no selection.
// We simulate the Windows Explorer/Nautilus behaviour of moving
// selection to this file.
if (FileHelper.isSelectable(triggerFile)) {
reduxDispatch(reduxActions.selectFiles({
fileIds: [payload.triggerFileId],
reset: true
}));
} else {
reduxDispatch(reduxActions.clearSelection());
}
}
}
reduxDispatch(reduxActions.showContextMenu({
triggerFileId: payload.triggerFileId,
mouseX: payload.clientX - 2,
mouseY: payload.clientY - 4
}));
})
};
var DefaultActions = {
/**
* Action that can be used to open currently selected files.
*/
OpenSelection: /*#__PURE__*/defineFileAction({
id: 'open_selection',
hotkeys: ['enter'],
requiresSelection: true,
fileFilter: FileHelper.isOpenable,
button: {
name: 'Open selection',
toolbar: true,
contextMenu: true,
group: 'Actions',
icon: exports.ChonkyIconName.openFiles
}
}, function (_ref) {
var state = _ref.state,
reduxDispatch = _ref.reduxDispatch;
reduxDispatch(thunkRequestFileAction(EssentialActions.OpenFiles, {
files: state.selectedFilesForAction
}));
return undefined;
}),
/**
* Action that selects all files.
*/
SelectAllFiles: /*#__PURE__*/defineFileAction({
id: 'select_all_files',
hotkeys: ['ctrl+a'],
button: {
name: 'Select all files',
toolbar: true,
contextMenu: true,
group: 'Actions',
icon: exports.ChonkyIconName.selectAllFiles
},
selectionTransform: function selectionTransform(_ref2) {
var fileIds = _ref2.fileIds,
hiddenFileIds = _ref2.hiddenFileIds;
var newSelection = new Set();
fileIds.map(function (fileId) {
// We don't need to check if file is selectable because Chonky does
// it own checks internally.