Repository: pqina/filepond Branch: master Commit: 990dca310c45 Files: 298 Total size: 1.3 MB Directory structure: gitextract_hbxyat0y/ ├── .babelrc ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ └── ISSUE_TEMPLATE/ │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── banner-cli.js ├── banner.js ├── dist/ │ ├── filepond.css │ ├── filepond.esm.js │ └── filepond.js ├── index.html ├── jest.config.js ├── jest.stubs.js ├── locale/ │ ├── am-et.js │ ├── ar-ar.js │ ├── az-az.js │ ├── ca-ca.js │ ├── cs-cz.js │ ├── cy-cy.js │ ├── da-dk.js │ ├── de-de.js │ ├── el-el.js │ ├── en-en.js │ ├── es-es.js │ ├── et-ee.js │ ├── fa_ir.js │ ├── fi-fi.js │ ├── fr-fr.js │ ├── he-he.js │ ├── hr-hr.js │ ├── hu-hu.js │ ├── id-id.js │ ├── it-it.js │ ├── ja-ja.js │ ├── km-km.js │ ├── ko-kr.js │ ├── ku-ckb.js │ ├── kur-ckb.js │ ├── lt-lt.js │ ├── lus-lus.js │ ├── lv-lv.js │ ├── nl-nl.js │ ├── no_nb.js │ ├── pl-pl.js │ ├── pt-br.js │ ├── pt-pt.js │ ├── ro-ro.js │ ├── ru-ru.js │ ├── sk-sk.js │ ├── sl-si.js │ ├── sr-rs.js │ ├── sv_se.js │ ├── tr-tr.js │ ├── uk-ua.js │ ├── ur-ur.js │ ├── vi-vi.js │ ├── zh-cn.js │ ├── zh-hk.js │ └── zh-tw.js ├── package.json ├── rollup.config.js ├── rollup.scripts.js ├── src/ │ ├── css/ │ │ ├── assistant.css │ │ ├── browser.css │ │ ├── data.css │ │ ├── drip.css │ │ ├── drop-label.css │ │ ├── file-action-button.css │ │ ├── file-info.css │ │ ├── file-status.css │ │ ├── file-wrapper.css │ │ ├── file.css │ │ ├── hopper.css │ │ ├── item-order.css │ │ ├── item.css │ │ ├── list-scroller.css │ │ ├── list.css │ │ ├── modifiers.css │ │ ├── panel-root.css │ │ ├── panel.css │ │ ├── progress-indicator.css │ │ ├── root-order.css │ │ ├── root.css │ │ └── styles.css │ └── js/ │ ├── __tests__/ │ │ ├── addFile.test.js │ │ ├── callbacks.test.js │ │ ├── contentDisposition.test.js │ │ ├── createInstance.test.js │ │ ├── removeFile.test.js │ │ ├── revertUploadOnRemove.test.js │ │ ├── server.test.js │ │ ├── setFiles.test.js │ │ └── windowMatchMedia.mock │ ├── app/ │ │ ├── actions.js │ │ ├── enum/ │ │ │ ├── ChunkStatus.js │ │ │ ├── FileOrigin.js │ │ │ ├── InteractionMethod.js │ │ │ ├── ItemStatus.js │ │ │ ├── Key.js │ │ │ ├── Status.js │ │ │ └── Type.js │ │ ├── frame/ │ │ │ ├── createPainter.js │ │ │ ├── createRoute.js │ │ │ ├── createStore.js │ │ │ ├── createView.js │ │ │ ├── index.js │ │ │ ├── mixins/ │ │ │ │ ├── animations.js │ │ │ │ ├── apis.js │ │ │ │ ├── index.js │ │ │ │ ├── listeners.js │ │ │ │ ├── styles.js │ │ │ │ └── utils/ │ │ │ │ └── addGetSet.js │ │ │ └── utils/ │ │ │ ├── AxisEnum.js │ │ │ ├── addEvent.js │ │ │ ├── animators/ │ │ │ │ ├── easing.js │ │ │ │ ├── spring.js │ │ │ │ └── tween.js │ │ │ ├── appendChild.js │ │ │ ├── appendChildView.js │ │ │ ├── createAnimator.js │ │ │ ├── createElement.js │ │ │ ├── getChildCount.js │ │ │ ├── getViewRect.js │ │ │ ├── removeChildView.js │ │ │ ├── removeEvent.js │ │ │ └── updateRect.js │ │ ├── index.js │ │ ├── options.js │ │ ├── queries.js │ │ ├── utils/ │ │ │ ├── buildURL.js │ │ │ ├── convertTo.js │ │ │ ├── createDragHelper.js │ │ │ ├── createFetchFunction.js │ │ │ ├── createFileLoader.js │ │ │ ├── createFileProcessor.js │ │ │ ├── createFileProcessorFunction.js │ │ │ ├── createFileStub.js │ │ │ ├── createHopper.js │ │ │ ├── createInitialState.js │ │ │ ├── createItem.js │ │ │ ├── createItemAPI.js │ │ │ ├── createOption.js │ │ │ ├── createOptionAPI.js │ │ │ ├── createOptionActions.js │ │ │ ├── createOptionQueries.js │ │ │ ├── createOptions.js │ │ │ ├── createPaster.js │ │ │ ├── createPerceivedPerformanceUpdater.js │ │ │ ├── createProcessorFunction.js │ │ │ ├── createRevertFunction.js │ │ │ ├── createServerAPI.js │ │ │ ├── dnd.js │ │ │ ├── dropAreaDimensions.js │ │ │ ├── dynamicLabel.js │ │ │ ├── fetchBlob.js │ │ │ ├── getActiveItems.js │ │ │ ├── getItemById.js │ │ │ ├── getItemByQuery.js │ │ │ ├── getItemIndexByPosition.js │ │ │ ├── getItemIndexByQuery.js │ │ │ ├── getItemsPerRow.js │ │ │ ├── getType.js │ │ │ ├── getValueByType.js │ │ │ ├── hasRoomForItem.js │ │ │ ├── insertItem.js │ │ │ ├── isAPI.js │ │ │ ├── mergeOptionObject.js │ │ │ ├── on.js │ │ │ ├── processFileChunked.js │ │ │ ├── removeReleasedItems.js │ │ │ ├── requestDataTransferItems.js │ │ │ └── toServerAPI.js │ │ └── view/ │ │ ├── assistant.js │ │ ├── blob.js │ │ ├── browser.js │ │ ├── data.js │ │ ├── drip.js │ │ ├── dropLabel.js │ │ ├── file.js │ │ ├── fileActionButton.js │ │ ├── fileInfo.js │ │ ├── fileStatus.js │ │ ├── fileWrapper.js │ │ ├── item.js │ │ ├── list.js │ │ ├── listScroller.js │ │ ├── panel.js │ │ ├── progressIndicator.js │ │ └── root.js │ ├── createApp.js │ ├── createAppAPI.js │ ├── createAppAtElement.js │ ├── createAppObject.js │ ├── createAppPlugin.js │ ├── filter.js │ ├── index.js │ └── utils/ │ ├── arrayInsert.js │ ├── arrayRemove.js │ ├── arrayReverse.js │ ├── attr.js │ ├── attrToggle.js │ ├── canUpdateFileInput.js │ ├── capitalizeFirstLetter.js │ ├── composeObject.js │ ├── copyFile.js │ ├── copyObjectPropertiesToObject.js │ ├── createBlob.js │ ├── createDefaultResponse.js │ ├── createElement.js │ ├── createObject.js │ ├── createResponse.js │ ├── createWorker.js │ ├── debounce.js │ ├── deepCloneObject.js │ ├── defineProperty.js │ ├── describeArc.js │ ├── forEachDelayed.js │ ├── forin.js │ ├── formatFilename.js │ ├── fromCamels.js │ ├── getAttributesAsObject.js │ ├── getBase64DataFromBase64DataURI.js │ ├── getBlobBuilder.js │ ├── getBlobFromBase64DataURI.js │ ├── getBlobFromByteStringWithMimeType.js │ ├── getByteStringFromBase64DataURI.js │ ├── getDateString.js │ ├── getDecimalSeparator.js │ ├── getDomainFromURL.js │ ├── getExtensionFromFilename.js │ ├── getFileFromBase64DataURI.js │ ├── getFileFromBlob.js │ ├── getFileInfoFromHeaders.js │ ├── getFilenameFromURL.js │ ├── getFilenameWithoutExtension.js │ ├── getMimeTypeFromBase64DataURI.js │ ├── getNonNumeric.js │ ├── getNumericAspectRatioFromString.js │ ├── getParameters.js │ ├── getRandomNumber.js │ ├── getRootNode.js │ ├── getThousandsSeparator.js │ ├── getUniqueId.js │ ├── guesstimateExtension.js │ ├── guesstimateMimeType.js │ ├── hasQueryString.js │ ├── insertAfter.js │ ├── insertBefore.js │ ├── isArray.js │ ├── isBase64DataURI.js │ ├── isBoolean.js │ ├── isBrowser.js │ ├── isDefined.js │ ├── isEmpty.js │ ├── isExternalURL.js │ ├── isFile.js │ ├── isFunction.js │ ├── isIOS.js │ ├── isInt.js │ ├── isNode.js │ ├── isNull.js │ ├── isNumber.js │ ├── isObject.js │ ├── isString.js │ ├── leftPad.js │ ├── limit.js │ ├── loadImage.js │ ├── lowerCaseFirstLetter.js │ ├── percentageArc.js │ ├── polarToCartesian.js │ ├── renameFile.js │ ├── replaceInString.js │ ├── resetFileInput.js │ ├── sendRequest.js │ ├── setInputFiles.js │ ├── text.js │ ├── toArray.js │ ├── toBoolean.js │ ├── toBytes.js │ ├── toCamels.js │ ├── toFloat.js │ ├── toFunctionReference.js │ ├── toInt.js │ ├── toNaturalFileSize.js │ ├── toNumber.js │ ├── toPercentage.js │ ├── toString.js │ └── trim.js └── types/ ├── index.d.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "retainLines": true, "presets": [ ["@babel/preset-env", { "exclude": ["transform-typeof-symbol"], "modules": false }] ], "plugins": [ ["@babel/plugin-proposal-object-rest-spread", { "loose": true, "useBuiltIns": true }], ["@babel/plugin-transform-template-literals", { "loose": true }] ], "env": { "test": { "presets": [ ["@babel/preset-env", { "targets": { "node": "current" } }] ] } } } ================================================ FILE: .gitattributes ================================================ dist/* linguist-vendored=false ================================================ FILE: .github/FUNDING.yml ================================================ custom: ['https://www.buymeacoffee.com/rikschennink'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report title: "[Bug] " description: Report an issue with FilePond labels: bug body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: checkboxes attributes: label: Is there an existing issue for this? description: Please search to see if an issue already exists for the bug you encountered. options: - label: I have searched the existing issues required: true - type: checkboxes attributes: label: Have you updated FilePond and its plugins? description: Please update both FilePond and its plugins to the most recent version. options: - label: I have updated FilePond and its plugins required: true - type: textarea attributes: label: Describe the bug description: A concise description of what the bug is. placeholder: Bug description validations: required: true - type: textarea attributes: label: Reproduction description: Provide clear steps to reproduce the bug. Please take the time to create a reproduction of the bug by [forking this project on codesandbox.io](https://codesandbox.io/s/filepond-plain-javascript-24i1m) placeholder: Reproduction validations: required: true - type: textarea attributes: label: Environment description: | examples: - **Device**: Samsung Galaxy s20, iPhone 12 Mini, Macbook Pro (2020), etc. - **OS**: Android 8, iOS 12, Windows 10, etc. - **Browser**: Firefox 93, Chrome 94, etc. value: | - Device: - OS: - Browser: render: markdown validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Stack Overflow url: https://stackoverflow.com/questions/tagged/filepond about: Ask questions on Stack Overflow and tag with "filepond" ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature request title: "[Feature] " description: Suggest an idea for this project labels: new feature body: - type: markdown attributes: value: | Thanks for taking the time to fill out this feature request! - type: checkboxes attributes: label: Is there an existing issue for this? description: Please search to see if an issue already exists for the feature request you want to submit. options: - label: I have searched the existing issues required: true - type: textarea attributes: label: Is your feature request related to a problem? Please describe. description: A clear and concise description of what the problem is you want to solve. placeholder: I'm always frustrated when... validations: required: true - type: textarea attributes: label: Describe the solution you'd like description: A clear and concise description of what you want to happen. validations: required: true - type: textarea attributes: label: Describe alternatives you've considered description: A clear and concise description of any alternative solutions or features you've considered. validations: required: true ================================================ FILE: .gitignore ================================================ npm-debug.log node_modules/ coverage/ .idea .vscode/ upload/ test*.html _TODO.md _RELEASE.md ================================================ FILE: .nvmrc ================================================ v10.15.3 ================================================ FILE: .prettierignore ================================================ dist/* ================================================ FILE: .prettierrc ================================================ { "svelteSortOrder": "scripts-markup-styles", "trailingComma": "es5", "tabWidth": 4, "printWidth": 100, "singleQuote": true, "semi": true } ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## 4.32.12 - Link attribution banner to [filepond.com](https://filepond.com) ## 4.32.11 - Merge fix file processing aborted and moving to next file. #1065 ## 4.32.10 - Added various new locales. - Merge fix for drop offset mismatch when scrolling. ## 4.32.9 - Added various new locales. ## 4.32.8 - Merged multiple PRs with various fixes. ## 4.32.7 - Fix issue where `checkValidity` would not reset field validity status after removing an invalid file. ## 4.32.6 - Fix `processfiles` event missing from types.` ## 4.32.5 - Attempt to fix item dragging issue on Android. ## 4.32.4 - Make credits link more visible to prevent authors from leaving it in place accidentally. ## 4.32.3 - Add nofollow to credits link. ## 4.32.2 - Fix accessibility issue with data fieldset missing a legend. ## 4.32.1 - Fix types. ## 4.32.0 - Add `onload` property to chunk patch request, receives `xhr` object, chunk index, and total chunks. #649 - Fix role attribute in voice over assistant to improve WCAG compatibility #988 ## 4.31.4 - Fix issue where images pasted in contenteditable elements would be handled by FilePond. ## 4.31.3 - Fix issue where hidden input fields in `filepond--data` fieldset would stay disabled after enabling FilePond. #1001 ## 4.31.2 - Remove `aria-hidden` from drop label as suggested by Lighthouse. - Fix `tabIndex` attribute on credits link not set correctly, now no longer included in tab flow. ## 4.31.1 - Fix issue with dragging file items sometimes not working. ## 4.31.0 - Add support for editing mock files if `item.source` is url. ## 4.30.6 - Fix issue where using a number as source for a local file would throw a `url.split` error. ## 4.30.5 - Fix file field value assignment #905 ## 4.30.4 - Fix a bug where the selected file is not replaced #841 - Fix a bug where the onwarning event is not triggered #839 - Add more translations ## 4.30.3 - Fix accidental push. ## 4.30.2 - Remove accidental log statement. ## 4.30.1 - Prioritize server prop before other props when passed to setOptions #606 ## 4.30.0 - Add `labelFileSizeBytes`, `labelFileSizeKilobytes`, `labelFileSizeMegabytes`, `labelFileSizeGigabytes` #763 ## 4.29.1 - Revert chunked uploads #757 ## 4.29.0 - Enable rejecting images with an error message using the internal `DID_LOAD_ITEM` hook. ## 4.28.2 - Fix issue where local server files could not be re-uploaded after editing and did not trigger remove of source file. ## 4.28.1 - Fix CSS error. ## 4.28.0 - Add `storeAsFile` property, if set to `true` FilePond will attempt to store the file objects in file input elements allowing file submit along with parent form (no need for `server` property). **This only works if the browser [supports the DataTransfer constructor](https://caniuse.com/mdn-api_datatransfer_datatransfer), this is the case on Firefox, Chrome, Chromium powered browsers and Safari version 14.1 and higher.** - Switch to PostCSS for style output. ## 4.27.3 - Fix issue with file.js component leaking state causing `allowRemove` to impact other instances of FilePond. #713 ## 4.27.2 - Fix issue with fetch and `HEAD` no setting server id to hidden input element. ## 4.27.1 - Fix issue with `allowMinimumUploadDuration` set to `false` throwing error. ## 4.27.0 - Add `allowMinimumUploadDuration` set to `false` to prevent a minimum upload time of 750ms. ## 4.26.2 - `setMetadata` internal `silent` bool now does fire internal `DID_UPDATE_ITEM_METADATA` but doesn't trigger upload or file prepare logic. This fixes an issue with the new image editor and file poster plugins. ## 4.26.1 - Add metdata change info to internal `SHOULD_PREPARE_OUTPUT` call ## 4.26.0 - Fix problem with rendering 0 items per row. #676 - The `headers` property of the `server.process` end point can now be a function. ## 4.25.3 - Fix issue with `chunkRetryDelays`. #671 ## 4.25.2 - Fix issue with fixed with file items not row wrapping correctly. #653 - Fix file info label when remove button positioned to the right. #620 ## 4.25.1 - Renamed `beforePrepareOutput` hook to `beforePrepareFile`. ## 4.25.0 - Add `beforePrepareOutput` hook to intercept and prevent preparing a new output file. ## 4.24.0 - Add action info to internal `SHOULD_PREPARE_OUTPUT` call - Moved edit button CSS to FilePond ## 4.23.1 - When files are dropped in a folder the file type cannot always be determined, this fix prevents FilePond from creating a Blob if it can't guesstimate the file type. ## 4.23.0 - Add "powered by" footer and [credits prop](https://pqina.nl/filepond/docs/patterns/api/filepond-instance/#disabling-credits). ## 4.22.1 - Fix problem with locale files. ## 4.22.0 - Add internal filter for plugins to manipulate dropped files before adding them to the files list. ## 4.21.1 - Fix calculation of file size when `fileSizeBase` is set to 1024. ## 4.21.0 - Add `fileSizeBase` use to adjust the way files sizes are displayed. Default is `1000`. ## 4.20.1 - Add `allowRemove` option so it actually works. ## 4.20.0 - Add `allowRemove`, set to `false` to disable remove button. - Improve TypeScript definitions. - Fix issue where `removeFiles` would not remove all files. ## 4.19.2 - Fix problem with locale files not ending up on npm. ## 4.19.1 - Fix issue where removal of a server file was requested when setting new files to the `files` property. ## 4.19.0 - Add locale folder, can now import different locales. - Improve `supports` method, now correctly detects MacOS Safari 8. - Improve type definitions file. - Fix issue with `removeFiles({ revert: false })` not working. - Fix issue where content pasted in a textarea would be interpreted as a file. ## 4.18.0 - Add fallback for `fetch` when loading remote URLs, if no custom fetch supplied, will use default request. - Add TypeScript dynamic label types. - Fix issue where order of files wasn't correct when setting initial files. ## 4.17.1 - Fix issue where reorder event was fired on each drag interaction, now only fires when order changes. ## 4.17.0 - Add `allowProcess`, set to `false` to remove processing button and related abort / retry processing controls. - Fix issue where hidden inputs didn't reflect visual order of files in list. ## 4.16.0 - Add `allowSyncAcceptAttribute`, set to `false` to prevent FilePond from setting the file input field `accept` attribute to the value of the `acceptedFileTypes`. ## 4.15.1 - Fix issue with abort being called even when not supplied. ## 4.15.0 - Add support for reording items in grid layout. Thanks @jwsinner ❤︎ ## 4.14.0 - Add `oninitfile`, called when file is first initialised, can be used to immediately set metadata. ## 4.13.7 - Fix backwards compatibility problem with `4.13.5` and `4.13.6` where removeFile would revert upload. - Add `{ revert: true }` as parameter to `removeFile` and `removeFiles` methods. Where in the previous two fix versions reverting was added to be done automatically this new parameter now needs be set to revert the upload. ## 4.13.6 - Fix problem where revert wasn't called for user added files. ## 4.13.5 - Fix trigger of revert handler to `removeFile` API. - Fix problem where circular layout wouldn't work on latest Safari. ## 4.13.4 - Fix issue where FilePond internal event mechanism would be in slowmotion mode when running in an inactive tab because of `setTimeout` use. ## 4.13.3 - Fix issue where FilePond would excessivly pause in between processing files while running in an inactive tab. ## 4.13.2 - Fix issue where FilePond running in an inactive tab would be very slow to pick up new files. ## 4.13.1 - Fix issue where HEAD fetch request would try turn response into zero byte file. ## 4.13.0 - Fix issue where hidden file fields were not in the correct order when files were sorted either automatically or manually. - Clean up accidental log statement left in 4.12.2 release. ## 4.12.2 - Fix issue with re-enabling FilePond field from disabled state not applying the appropriate fields to the browse input. ## 4.12.1 - Fix issue where browse button wasn't clickable when `styleLayoutMode` was set to `compact`. ## 4.12.0 - Add `styleButtonRemoveItemAlign` to align remove button to the left side of the file item. - Fix issue where list of files could not be scrolled when FilePond was disabled. ## 4.11.0 - Add `relativePath` property to file item. - Add `onreorderfiles` callback. - Fix issue where unkown type was `"null"` instead of an empty string. - Fix issue where `onactivatefile` was fired on drag end. ## 4.10.0 - Copy webkitDirectory property to file object. ## 4.9.5 - Fix issue with error format in TypeScript types. ## 4.9.4 - Fix problem with API querystring containing multiple questionmarks. ## 4.9.3 - Fix problem where ending the class attribute on a space would throw an error. ## 4.9.2 - Add `grab` cursor to items so there's and indicator that items are grabbable. ## 4.9.1 - Fix issue where Chrome on Android would launch pull-to-refresh when trying to drag a file item. ## 4.9.0 - Add drag to reorder file items, enable by setting `allowReorder` to `true`. - Only works in single column mode (for now). - It also works when the list of files is showing a scrollbar, but dragging + scrolling isn't working correctly at the moment. - Limited to [browsers supporting Pointer events](https://caniuse.com/#feat=pointer). - Add `moveFile(query, index)` method. Use to move a file to a different index in the file items array. ## 4.8.2 - Fix problem with 4.8.1 fix not working with SSR. ## 4.8.1 - Fix IE issue where adding markup would not work. ## 4.8.0 - Add `prepareFile` and `prepareFiles` methods to the FilePond instance, use to request output files of the current items in the files list. ## 4.7.4 - UTF-8 encode request headers to prevent issues with weird characters. ## 4.7.3 - Switch from `setAttribute` to `cssText` for layout changes resulting in better performance and CSP compatibility. [#400](https://github.com/pqina/filepond/pull/400) ## 4.7.2 - Fix issue where iOS 10 would throw an error when calling `delete` on a dataset property - Fix issue with `onwarning` being called on incorrect element ## 4.7.1 - Fix problem where directories with over 100 files weren't read correctly. ## 4.7.0 - Add support for [chunked uploads](https://pqina.nl/filepond/docs/patterns/api/server/#process-chunks). Thanks to Ryan Olson (@ams-ryanolson) Arctic Media for donating the funds to build this. ## 4.6.1 - Add missing Blob type to TypeScript server config. ## 4.6.0 - Add TypeScript declarations. ## 4.5.2 - If `Blob` has `name` attribute use name attribute instead of URL for file name. ## 4.5.1 - Fix issue where drag-drop from Firefox download list would not add file to drop area ## 4.5.0 - Add option to set server end point headers on a generic level so they're applied to all end points using `server.headers`. ## 4.4.13 - Fix problem with CSS overriding image preview markup text size. ## 4.4.12 - Fix memory leak. ## 4.4.11 - Fix problem with abort statement in file loader logic. ## 4.4.10 - Fix issue where Promise returned by `addFile` would not be rejected if file failed to load. ## 4.4.9 - Fix security issues with dependencies. ## 4.4.8 - Fix issue where multiple calls to `setMetadata` would result in multiple successive calls to prepare file. - Fix issue where drop area aspect ratio would not update correctly on resize. ## 4.4.7 - Fix issue where pasting a file would throw an error. - Fix issue where ignored files would be counted as files when dropping a folder. ## 4.4.6 - Fix issue where `processFiles` would re-process `local` server images. ## 4.4.5 - Fix issue where FilePond event loop would freeze when tab was inactive. ## 4.4.4 - Fix issue where FilePond would not render when hidden, resulting in missing input elements and events not firing. ## 4.4.3 - Fix issue where processing the queue didn't work correctly when files were removed while being in the queue. ## 4.4.2 - Fix issue where UTF-8 encoded filename was not parsed correctly. ## 4.4.1 - Fix issue where `Content-Disposition` header filename was not parsed correctly. ## 4.4.0 - Fix issue where `addFile` did not respect `itemInsertLocation` setting. - Add the `beforeDropFile` hook which can be used to validate a dropped item before it's added, make sure `dropValidation` is set to `true` as well. ## 4.3.9 - Fix problem where enabling FilePond after being `disabled` would not allow browsing for files. ## 4.3.8 - Improve accessibility of buttons by moving label from `title` to inner hidden ``. ## 4.3.7 - Attempt #2 at fixing the issue of release `4.3.6`. ## 4.3.6 - Fix problem where the `abortAll` call triggered when destroying FilePond would inadvertently trigger the `server.remove` end point for each local file. ## 4.3.5 - Fix issue where changing the `stylePanelAspectRatio` would not update the container size. ## 4.3.4 - Add source code. - Add build scripts. - Fix `onremovefile` callback not receiving an error object similar to `onaddfile`. ## 4.3.3 - Fix issue where aborting a file load while the file was being prepared (for instance, encoded) did not work. ## 4.3.2 - Fix issue where 0 byte files would not upload to the server. ## 4.3.1 - Add `status` property to the FilePond instance, use this property to determine the current FilePond status (`EMPTY`, `IDLE`, `ERROR`, `BUSY`, or `READY`). ## 4.3.0 - Fix problem where `addFiles` would not correctly map passed options to files. - Fix problem where upload error would prevent processing of other files. - Fix problem where the field would not "exist" if it had no value. Now if FilePond is empty the internal file input element is given the name attribute, this is removed when a file is added (as the name is then present on the file's hidden input element). - Add `onprocessfiles` which is called when all files have been processed. - Add `onactivatefile` which is called when a user clicks or taps on a file item. ## 4.2.0 - Add `disabled` property, can be set as an attribute on the file input or as a property in the FilePond options object. - Add catching clicks on the entire pond label element to make it easier to click the label. ## 4.1.4 - Only hide preview images when resizing the window horizontally, fixes problem with resize events on iOS. ## 4.1.3 - Improve the way that FilePond resumes drawing when a tab retains focus. ## 4.1.2 - Fix problem where `onaddfile` callback parameters were reversed when file validation plugins prevented file load ## 4.1.1 - Fix problem where error shake animation would mess up preview image. ## 4.1.0 - Add `itemInsertLocationFreedom` property, set to `false` to stop user from picking the location in the file list where the file is added. ## 4.0.2 - Fix problem with undefine `ItemStatus` object in `processFiles` method ## 4.0.1 - Fix problem where window resize handler was removed incorrectly resulting in an error. ## 4.0.0 Multiple improvements, small fixes and new features. As updating will result in animation speed changes, changes to the way files are added to the files list, and will require an update of the image preview plugin, the version has been bumped to 4.0.0 - Add grid layout feature. Assign a fixed width to a filepond item and FilePond will render the items in rows. The code below will render a list view on small viewports, a 50/50 grid on medium viewports, and a 33/33/33 grid on wide viewports. The `.5em` in each calc statement is equivalent to the combined left and right margin of each filepond item. ```css @media (min-width: 30em) { .filepond--item { width: calc(50% - 0.5em); } } @media (min-width: 50em) { .filepond--item { width: calc(33.33% - 0.5em); } } ``` - Add `styleItemPanelAspectRatio` to control the item panel aspect ratio and render item panels in a fixed size. - Add `sort` method on FilePond instance for sorting FilePond files. - Add `itemInsertLocation` property to set default insert location of files or sort method. - Add `itemInsertInterval` to control the small delay between adding items to the files list. - Improve drag and drop performance. - Improve file insert logic and performance. - Improve rendering of file previews will now scale correctly when window is resized. - Improve handling of dropped directories on Firefox, file type was missing, now guestimates file type based on file extension. - Small tweaks and changes to file animation durations and intros. - Fixed drag coordinates being slightly out of place. - Multiple small fixes and code improvements. ## 3.9.0 - Add `checkValidity` which is set to `false` by default. If it's set to `true`, FilePond will set the contents of the `labelInvalidField` property as the field custom validity message if it contains invalid files (files that for instance exceed max file size or fail other tests). ## 3.8.2 - Fix problem where remove server error message was passed directly to client without label. Set `labelFileRemoveError` to a string to change default error, set it to a function to show custom server error. `{ labelFileRemoveError: serverError => serverError }` ## 3.8.1 - Expose `dispatch` call to plugin item extensions. ## 3.8.0 - Add `forceRevert` option, set to `true` to force a revert action to finish before continuing. ## 3.7.7 - Improve `onlistupdate` event so it can be better synced with adapter components. ## 3.7.6 - Switched browse text underline to `text-decoration-skip-ink: auto` instead of `text-decoration-skip: ink` to prevent eslint warnings. ## 3.7.5 - Fix problem where calling `processFiles` without arguments would re-process already processed files. ## 3.7.4 - Fix problem where subsequent calls to `processFile` would not automatically revert an uploaded file or abort an active upload. ## 3.7.3 - Fix problem where upload complete indicator would not show when image preview was active. ## 3.7.2 - Expose `createItemAPI` to plugins. ## 3.7.1 - Fix problem where URLs would immidiately be in processed state. ## 3.7.0 - Add `maxParallelUploads` option to limit the amount of files being uploaded in parallel. - Add option to only fetch file head when downloading remote URLs. File is downloaded to the server and server sends a unique file id to the client. Set `server.fetch.method` to `'HEAD'` the server needs to repond with custom header `X-Content-Transfer-Id` and a unique id. See [handle_fetch_remote_file](https://github.com/pqina/filepond-server-php/blob/master/index.php#L91) in FilePond PHP Server for an example implementation. ## 3.6.0 - Add support for uploading transform plugin variants. - Add `server.process.ondata` which allows adding entries to the formdata before it's sent to the server. ## 3.5.1 - Fix problem where `processFile` and `processFiles` would reprocess already processed files. ## 3.5.0 - Add `beforeAddFile` hook, this can be used to quickly validate files before they're being added. ## 3.4.0 - Add `server.remove` property, this property can be optionally set to a method to call when the remove button is tapped on a `local` file. This allows removing files from the server. Please note that allowing clients to remove files from the server is a potential security risk and requires extra caution. By default the property is `null`. The advise is to not use this method and only make changes to the server after the parent form has been submitted. The form POST will contain all the loaded file names and relevant file data, it should be enough to determine the files to remove and the files to keep. ## 3.3.3 - Fix filename matching of content-disposition header when the filename is not wrapped in quotes. - Fix problem where special characters in filename prevented a file from being added ## 3.3.2 - Fix problem where revert call would revert wrong file item. ## 3.3.1 - Fix problem where exceeding the max file limit would not throw an error ## 3.3.0 - Add feature to silently update metadata so it doesn't trigger an update. ## 3.2.5 - Fix issue where items would be removed before item sub views were all in rest state. ## 3.2.4 - Fix problem where server side rendering would not work correctly. ## 3.2.3 - Fix problem where `beforeRemoveFile` hook was not called when in `instantUpload` mode and reverting an upload. ## 3.2.2 - Add preparations for queueing file processing. - Improve guards against errors when items are removed. - Improve alignment of drop label. ## 3.2.1 - Group updateitems callback for better compatibility with React. ## 3.2.0 - Add global scoped property for painter so multiple libraries can subscribe to read and write DOM operations. This is mostly in preparation for a standalone version of the Image Editor plugin. ## 3.1.6 - Fix problem where remove callback would no longer work. ## 3.1.5 - Fix problem with WebWorkers not working correctly on Edge and IE. ## 3.1.4 - Fix syntax error [#147](https://github.com/pqina/filepond/pull/147) ## 3.1.3 - Fix additional problem with quick file removals. ## 3.1.2 - Fix problem where remove call would throw error depending on the state of the upload. - Fix problem where clicking on abort before upload had started would not cancel upload. Please note that this update will require installing new versions of the following plugins: - File Validate Size ## 3.1.1 - Fix problem where panel overflow would render incorrectly. ## 3.1.0 - Improve diffing when updating the `files` property. - Add `onupdatefiles` callback that is triggered when a file is added or removed to a pond instance. ## 3.0.4 - Fix problem where feature detection would throw error on iOS 8.x ## 3.0.3 - Fix problem with XMLHttpRequest timeout on Internet Explorer 11. - Fix problem with custom properties on element on Internet Explorer 11. ## 3.0.2 - Fix problem with label not being clickable while in integrated layout mode. ## 3.0.1 - Fix problem where timeout would incorrectly trigger for uploads. ## 3.0.0 - Small internal flow changes to facilitate integration with the Image Editor plugin - Improve performance - Improve file loader so it now supports `blob` URLs - Add `stylePanelLayout` setting to set layout mode for panel - Add `stylePanelAspectRatio` setting to fix aspect ratio of panel - Add `styleButtonRemoveItemPosition` to control remove button position on image preview - Add `styleButtonProcessItemPosition` to control item processing position on image preview - Add `styleLoadIndicatorPosition` to control load indicator position on image preview - Add `styleProgressIndicatorPosition` to control process indicator position on image preview - Add method to automatically update data when file metadata is updated - Fix animation rest state detection Please note that this update will require installing new versions of the following plugins: - File Encode - Image Crop - Image Preview - Image Transform ## 2.3.1 - Fix improved browser environment detection. ## 2.3.0 - Improve browser environment detection [#123](https://github.com/pqina/filepond/pull/123). - Add `beforeRemoveFile` callback to allow user confirmation before actual file removal. ## 2.2.1 - Fix another problem where list overflow would not render correctly. ## 2.2.0 - Fix problem where `maxFiles` was not enforced when dropping a set of files, each file was added in sequence till `maxFiles` was reached while the set as a whole should've been invalidated at once. ## 2.1.3 - Fix problem where max-height of filepond root would not be respected by file list. ## 2.1.2 - Cleaned up some stray babelHelpers. - Fix bug in render engine style method, should result in less unnecessary redraws. ## 2.1.1 - Fix problem where the drop indicator would render at the wrong location. - Fix problem where calling `removeFile` directly after `processFile` was resolved would throw an error. ## 2.1.0 - Labels can now be set as functions, these functions will receive context information, this is useful to customize both he load error and processing error labels based on server response. ## 2.0.1 - Add additional utilities to plugin API. ## 2.0.0 - Automatically replace undo button counterclockwise arrow icon with remove button icon when `instantUpload` is set to `true`. ## 1.8.8 - Add `metadata` handling to `addFile` method. ## 1.8.7 - Fix problem where setting `allowRevert` to `false` would hide the remove button. ## 1.8.6 - Fix problem where adding dataURIs would throw an error ## 1.8.5 - Fix casting of input attributes without value to correct boolean ## 1.8.4 - Fix problem where response timeout would throw an error - Improve handling of returned value by processing onload function ## 1.8.3 - Cleaning up some stray code ## 1.8.2 - Fix problem where element options did not override page level options ## 1.8.1 - Handle `Blob` with `name` property same as actual `File` object - Attempt to fix a problem where `elementFromPoint` could not be found in Angular component ## 1.8.0 - Add view filter to file info view - Add option to [mock server files](https://pqina.nl/filepond/docs/patterns/api/filepond-object/#creating-a-filepond-instance) - Add option to [set initial file metadata](https://pqina.nl/filepond/docs/patterns/api/filepond-object/#creating-a-filepond-instance) for server files - Bugfixes ## 1.7.4 - Fix handling of `Content-Disposition` header to better extract the filename ## 1.7.3 - Events are now fired asynchronous, this allows internal processes to finish up ## 1.7.2 - Fix broken links in README ## 1.7.1 - Add view filter to file status view - Fix problem where attribute object value was not read correctly ## 1.7.0 - Add `onerror` callback to server configuration to allow custom parsing of error response ## 1.6.2 - Fix problem where restored temp file would not be removed correctly ## 1.6.1 - Add `FileOrigin` enum to FilePond object ## 1.6.0 - Add `fileOrigin` property to file item ## 1.5.4 - Update README with links to new plugins and adapters ## 1.5.3 - Accidentally skip over this version number ## 1.5.2 - Remove max-width on file status view ## 1.5.1 - Prevent text wrapping for file size label ## 1.5.0 - Add `onload` method to server configuration ## 1.4.1 - Fix progress indicator getting stuck on subsequent uploads ## 1.4.0 - Add `allowRevert` option to disable revert button ## 1.3.0 - Add `dropValidation` option to enable pre-validating of dropped items ## 1.2.11 - Improve timing of CPU heavy operations (like file encoding) when a file is added to FilePond ## 1.2.10 - Fix `removeFiles` method. Did not correctly remove files when called with empty arguments or an array of indexes ## 1.2.9 - Fix bug where `processFiles` only worked when receiving parameters ## 1.2.8 - Tiny improvements so can be used when server side rendering ## 1.2.7 - Improve loading indicator state - Add Angular adapter reference to README ## 1.2.6 - Add `onprocessfile` callback to options object ## 1.2.5 - Fix id attribute not being available on FilePond root - Fix FilePond not rendering correctly when initially hidden - Improve render performance ## 1.2.4 - Fix `setOptions` method not correctly converting value types ## 1.2.3 - Update the `OptionTypes` property when a plugin is registered ## 1.2.2 - Fix `setOptions` returning an incorrectly formatted options object ## 1.2.1 - Add README link to backlog on WIP - Fix problem where `destroy` would not remove FilePond - Switch license from GPL to MIT - Add jQuery to adapters list - Fix error message animation - Improve FilePond event parameters ## 1.1.0 - Accidentally skip version 1.1.0 ## 1.0.8 - Fix problem where plugins could be registered twice - Improve `files` property, now compares existing files against new files and updates accordingly ## 1.0.7 - Hide center panel before scaling panel view ## 1.0.6 - Improve style possibilities for panel views and enforce internal panel layout properties ## 1.0.5 - Fix problem where loading indicators would not spin for certain requests - Various improvements to README ## 1.0.4 - Fix render bugs - Improve panel view layout ## 1.0.3 - Fix processing complete state file item color ## 1.0.2 - Add support for client side file manipulation - Add support for file metadata - Improve performance ## 1.0.1 - Add correct banners to library files ## 1.0.0 - Initial release ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Pqina Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # [FilePond](https://pqina.nl/filepond/) A JavaScript library that can upload anything you throw at it, optimizes images for faster uploads, and offers a great, accessible, silky smooth user experience. [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/pqina/filepond/blob/master/LICENSE) [![npm version](https://badge.fury.io/js/filepond.svg)](https://www.npmjs.com/package/filepond) ![npm](https://img.shields.io/npm/dt/filepond) [![minzipped size](https://img.shields.io/bundlephobia/minzip/filepond)](https://bundlephobia.com/package/filepond) [![Discord](https://img.shields.io/discord/1422126182924554291?label=discord)](https://discord.gg/KTyymsrTrX) FilePond adapters are available for **[React](https://github.com/pqina/react-filepond)**, **[Vue](https://github.com/pqina/vue-filepond)**, **[Angular](https://github.com/pqina/ngx-filepond)**, **[Svelte](https://github.com/pqina/svelte-filepond)**, and **[jQuery](https://github.com/pqina/jquery-filepond)** [FilePond v5 Alpha version now available for testing](https://github.com/pqina/filepond/tree/v5) [Documentation](https://pqina.nl/filepond/docs) • [Discord](https://discord.gg/KTyymsrTrX) • [Examples](#live-demos) --- [FilePond](https://www.buymeacoffee.com/rikschennink/) [Buy me a Coffee](https://www.buymeacoffee.com/rikschennink/) • [Use FilePond with Pintura](https://pqina.nl/pintura/?ref=github-filepond) • [Dev updates](https://rik.schenn.ink) --- ### Core Features - Accepts **directories**, **files**, blobs, local URLs, **remote URLs** and Data URIs. - **Drop files**, select on filesystem, **copy and paste files**, or add files using the API. - **Async uploads** with AJAX, supports **chunk uploads**, can encode files as base64 data and send along form post. - **Accessible**, tested with AT software like VoiceOver and JAWS, **navigable by Keyboard**. - **Image optimization**, automatic image resizing, **cropping**, filtering, and **fixes EXIF orientation**. - **Responsive**, automatically scales to available space, is functional on both **mobile and desktop devices**. [Learn more about FilePond](https://pqina.nl/filepond/) [](https://pqina.nl/filepond/) --- ### Also need Image Editing? **Pintura the modern JavaScript Image Editor** is what you're looking for. Pintura supports setting **crop aspect ratios**, **resizing**, **rotating**, **cropping**, and **flipping** images. Above all, it integrates beautifully with FilePond. [Learn more about Pintura](https://pqina.nl/pintura/?ref=github-filepond) [](https://pqina.nl/pintura/?ref=github-filepond) --- ### Live Demos - [React](https://stackblitz.com/github/pqina/pintura-example-react?file=src%2FExampleFilePond.js) - [Angular](https://stackblitz.com/github/pqina/pintura-example-angular?file=src%2Fapp%2Ffilepond-example%2Ffilepond-example.component.ts) - [Svelte](https://stackblitz.com/github/pqina/pintura-example-svelte?file=src%2FApp.svelte%3AL152) - [Vue](https://stackblitz.com/github/pqina/pintura-example-vue-3?file=src%2FExampleFilePond.vue) ### Plugins - [File encode](https://github.com/pqina/filepond-plugin-file-encode) - [File rename](https://github.com/pqina/filepond-plugin-file-rename) - [File size validation](https://github.com/pqina/filepond-plugin-file-validate-size) - [File type validation](https://github.com/pqina/filepond-plugin-file-validate-type) - [File metadata](https://github.com/pqina/filepond-plugin-file-metadata) - [File poster](https://github.com/pqina/filepond-plugin-file-poster) - [Image editor](https://github.com/pqina/filepond-plugin-image-edit) - [Image size validation](https://github.com/pqina/filepond-plugin-image-validate-size) - [Image preview](https://github.com/pqina/filepond-plugin-image-preview) - [Image crop](https://github.com/pqina/filepond-plugin-image-crop) - [Image filter](https://github.com/pqina/filepond-plugin-image-filter) - [Image resize](https://github.com/pqina/filepond-plugin-image-resize) - [Image transform](https://github.com/pqina/filepond-plugin-image-transform) - [Image EXIF orientation](https://github.com/pqina/filepond-plugin-image-exif-orientation) - [Image overlay](https://github.com/nielsboogaard/filepond-plugin-image-overlay) ([nielsboogaard/filepond-plugin-image-overlay](https://github.com/nielsboogaard/filepond-plugin-image-overlay)) - [Media preview](https://github.com/nielsboogaard/filepond-plugin-media-preview) ([nielsboogaard/filepond-plugin-media-preview](https://github.com/nielsboogaard/filepond-plugin-media-preview)) - [Media preview + PDF preview](https://github.com/ErnestBrandi/filepond-plugin-media-preview) ([ErnestBrandi/filepond-plugin-media-preview](https://github.com/ErnestBrandi/filepond-plugin-media-preview)) - [Get file](https://github.com/nielsboogaard/filepond-plugin-get-file) ([nielsboogaard/filepond-plugin-get-file](https://github.com/nielsboogaard/filepond-plugin-get-file)) - [Zip Directory Uploads](https://github.com/tzsk/filepond-plugin-zipper) ([tzsk/filepond-plugin-zipper](https://github.com/tzsk/filepond-plugin-zipper)) - [PDF Preview](https://github.com/Adri-Glez/filepond-plugin-pdf-preview) ([Adri-Glez/filepond-plugin-pdf-preview](https://github.com/Adri-Glez/filepond-plugin-pdf-preview)) - [PDF Convert](https://github.com/alexandreDavid/filepond-plugin-pdf-convert) ([alexandreDavid/filepond-plugin-pdf-convert](https://github.com/alexandreDavid/filepond-plugin-pdf-convert)) ### Adapters - [React](https://github.com/pqina/react-filepond) - [Vue](https://github.com/pqina/vue-filepond) - [Svelte](https://github.com/pqina/svelte-filepond) - [jQuery](https://github.com/pqina/jquery-filepond) - [Angular](https://github.com/pqina/ngx-filepond) - [Angular 1](https://github.com/johnnyasantoss/angularjs-filepond) ([johnnyasantoss/angularjs-filepond](https://github.com/johnnyasantoss/angularjs-filepond)) - [Blazor](https://github.com/soenneker/soenneker.blazor.filepond) ([soenneker/soenneker.blazor.filepond](https://github.com/soenneker/soenneker.blazor.filepond)) - [Ember](https://github.com/alexdiliberto/ember-filepond) ([alexdiliberto/ember-filepond](https://github.com/alexdiliberto/ember-filepond)) ### Backend - [PHP](https://github.com/pqina/filepond-boilerplate-php) - [Django](https://github.com/ImperialCollegeLondon/django-drf-filepond) ([ImperialCollegeLondon/django-drf-filepond](https://github.com/ImperialCollegeLondon/django-drf-filepond)) - [Laravel](https://github.com/Sopamo/laravel-filepond) ([Sopamo/laravel-filepond](https://github.com/Sopamo/laravel-filepond)) - [Laravel](https://github.com/Albert221/laravel-filepond) ([Albert221/laravel-filepond](https://github.com/Albert221/laravel-filepond)) - [SilverStripe](https://github.com/lekoala/silverstripe-filepond) ([lekoala/silverstripe-filepond](https://github.com/lekoala/silverstripe-filepond)) - [Ruby on Rails](https://github.com/Code-With-Rails/filepond-rails) ([Code-With-Rails/filepond-rails](https://github.com/Code-With-Rails/filepond-rails)) ## Quick Start Install using npm: ```bash npm install filepond ``` Then import in your project: ```js import * as FilePond from 'filepond'; // Create a multi file upload component const pond = FilePond.create({ multiple: true, name: 'filepond', }); // Add it to the DOM document.body.appendChild(pond.element); ``` Or get it from a CDN: ```html FilePond from CDN ``` [Getting started with FilePond](https://pqina.nl/filepond/docs/patterns/getting-started/) ## Internationalization The [locale folder](./locale/) contains different language files, PR's are welcome, you can use locale files like this: ```js import pt_BR from 'filepond/locale/pt-br.js'; FilePond.setOptions(pt_BR); ``` ## Contributing At the moment test coverage is not great, it's around 65%. To accept pull requests the tests need to be better, any help to improve them is very much appreciated. Tests are based on Jest and can be run with `npm run test` To build the library run `npm run build` ## Publications - [Using FilePond with NodeJS](https://www.infoworld.com/article/3627248/how-to-use-filepond-with-nodejs.html) - [Applying Watermarks to Images with FilePond](https://pqina.nl/blog/applying-watermarks-to-images-with-filepond/) - [Generating Image Thumbnails in the Browser using JavaScript and FilePond](https://dev.to/pqina/generating-image-thumbnails-in-the-browser-using-javascript-and-filepond-10b8) - [How to upload files with Vue and FilePond](https://dev.to/pqina/how-to-upload-files-with-vue-and-filepond-1m02) - [Smooth file uploading with React and FilePond](https://itnext.io/uploading-files-with-react-and-filepond-f8a798308557) - [5 interesting technical challenges I faced while building FilePond](https://itnext.io/filepond-frontend-trickery-a3073c934c77) - [Image uploads with Laravel and FilePond](https://devdojo.com/episode/image-uploads-with-laravel-and-filepond) - [Integrating FilePond with Ember](https://alexdiliberto.com/ember-filepond/) - [FilePond launch day post-mortem](https://pqina.nl/blog/filepond-launch-day-post-mortem) - [FilePond on ProductHunt](https://www.producthunt.com/posts/filepond-js) ### Browser Compatibility FilePond is compatible with a wide range of desktop and mobile browsers, the oldest explicitly supported browser is IE11, for best cross browser support add [FilePond Polyfill](https://github.com/pqina/filepond-polyfill) and [Babel polyfill](https://babeljs.io/docs/en/babel-polyfill) to your project. FilePond uses [BrowserStack](https://www.browserstack.com/) for compatibility testing. [BrowserStack](https://www.browserstack.com/) ## License **Please don't remove or change the disclaimers in the source files** MIT License Copyright (c) 2020 PQINA | [Rik Schennink](mailto:rik@pqina.nl) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: banner-cli.js ================================================ const banner = require('./banner'); const pkg = require('./package.json'); const args = process.argv.slice(2); process.stdin.resume(); process.stdin.setEncoding('utf8'); process.stdin.on('data', function(data) { process.stdout.write(banner({ id: args[0], ...pkg }) + data); }); ================================================ FILE: banner.js ================================================ module.exports = ({ id, version, license, homepage }) => `/*! * ${ id } ${ version } * Licensed under ${ license }, https://opensource.org/licenses/${ license }/ * Please visit ${ homepage } for details. */ /* eslint-disable */ `; ================================================ FILE: dist/filepond.css ================================================ /*! * FilePond 4.32.12 * Licensed under MIT, https://opensource.org/licenses/MIT/ * Please visit https://pqina.nl/filepond/ for details. */ /* eslint-disable */ .filepond--assistant { position: absolute; overflow: hidden; height: 1px; width: 1px; padding: 0; border: 0; clip: rect(1px, 1px, 1px, 1px); -webkit-clip-path: inset(50%); clip-path: inset(50%); white-space: nowrap; } /* Hard to override styles */ .filepond--browser.filepond--browser { /* is positioned absolute so it is focusable for form validation errors */ position: absolute; margin: 0; padding: 0; /* is positioned ~behind drop label */ left: 1em; top: 1.75em; width: calc(100% - 2em); /* hide visually */ opacity: 0; font-size: 0; /* removes text cursor in Internet Explorer 11 */ } .filepond--data { position: absolute; width: 0; height: 0; padding: 0; margin: 0; border: none; visibility: hidden; pointer-events: none; contain: strict; } .filepond--drip { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; opacity: 0.1; /* can't interact with this element */ pointer-events: none; /* inherit border radius from parent (needed for drip-blob cut of) */ border-radius: 0.5em; /* this seems to prevent Chrome from redrawing this layer constantly */ background: rgba(0, 0, 0, 0.01); } .filepond--drip-blob { position: absolute; -webkit-transform-origin: center center; transform-origin: center center; top: 0; left: 0; width: 8em; height: 8em; margin-left: -4em; margin-top: -4em; background: #292625; border-radius: 50%; /* will be animated */ will-change: transform, opacity; } .filepond--drop-label { position: absolute; left: 0; right: 0; top: 0; margin: 0; color: #4f4f4f; /* center contents */ display: flex; justify-content: center; align-items: center; /* fixes IE11 centering problems (is overruled by label min-height) */ height: 0px; /* dont allow selection */ -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; /* will be animated */ will-change: transform, opacity; } /* Hard to override styles on purpose */ .filepond--drop-label.filepond--drop-label label { display: block; margin: 0; padding: 0.5em; /* use padding instead of margin so click area is not impacted */ } .filepond--drop-label label { cursor: default; font-size: 0.875em; font-weight: normal; text-align: center; line-height: 1.5; } .filepond--label-action { text-decoration: underline; -webkit-text-decoration-skip: ink; text-decoration-skip-ink: auto; -webkit-text-decoration-color: #a7a4a4; text-decoration-color: #a7a4a4; cursor: pointer; } .filepond--root[data-disabled] .filepond--drop-label label { opacity: 0.5; } /* Hard to override styles */ .filepond--file-action-button.filepond--file-action-button { font-size: 1em; width: 1.625em; height: 1.625em; font-family: inherit; line-height: inherit; margin: 0; padding: 0; border: none; outline: none; will-change: transform, opacity; /* hidden label */ } .filepond--file-action-button.filepond--file-action-button span { position: absolute; overflow: hidden; height: 1px; width: 1px; padding: 0; border: 0; clip: rect(1px, 1px, 1px, 1px); -webkit-clip-path: inset(50%); clip-path: inset(50%); white-space: nowrap; } .filepond--file-action-button.filepond--file-action-button { /* scale SVG to fill button */ } .filepond--file-action-button.filepond--file-action-button svg { width: 100%; height: 100%; } .filepond--file-action-button.filepond--file-action-button { /* bigger touch area */ } .filepond--file-action-button.filepond--file-action-button::after { position: absolute; left: -0.75em; right: -0.75em; top: -0.75em; bottom: -0.75em; content: ''; } /* Soft styles */ .filepond--file-action-button { /* use default arrow cursor */ cursor: auto; /* reset default button styles */ color: #fff; /* set default look n feel */ border-radius: 50%; background-color: rgba(0, 0, 0, 0.5); background-image: none; /* we animate box shadow on focus */ /* it's only slightly slower than animating */ /* a pseudo-element with transforms and renders */ /* a lot better on chrome */ box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); transition: box-shadow 0.25s ease-in; } .filepond--file-action-button:hover, .filepond--file-action-button:focus { box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.9); } .filepond--file-action-button[disabled] { color: rgba(255, 255, 255, 0.5); background-color: rgba(0, 0, 0, 0.25); } .filepond--file-action-button[hidden] { display: none; } /* edit button */ .filepond--action-edit-item.filepond--action-edit-item { width: 2em; height: 2em; padding: 0.1875em; } .filepond--action-edit-item.filepond--action-edit-item[data-align*='center'] { margin-left: -0.1875em; } .filepond--action-edit-item.filepond--action-edit-item[data-align*='bottom'] { margin-bottom: -0.1875em; } .filepond--action-edit-item-alt { border: none; line-height: inherit; background: transparent; font-family: inherit; color: inherit; outline: none; padding: 0; margin: 0 0 0 0.25em; pointer-events: all; position: absolute; } .filepond--action-edit-item-alt svg { width: 1.3125em; height: 1.3125em; } .filepond--action-edit-item-alt span { font-size: 0; opacity: 0; } .filepond--file-info { position: static; display: flex; flex-direction: column; align-items: flex-start; flex: 1; margin: 0 0.5em 0 0; min-width: 0; /* will be animated */ will-change: transform, opacity; /* can't do anything with this info */ pointer-events: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; /* no margins on children */ } .filepond--file-info * { margin: 0; } .filepond--file-info { /* we don't want to have these overrules so these selectors are a bit more specific */ } .filepond--file-info .filepond--file-info-main { font-size: 0.75em; line-height: 1.2; /* we want ellipsis if this bar gets too wide */ text-overflow: ellipsis; overflow: hidden; white-space: nowrap; width: 100%; } .filepond--file-info .filepond--file-info-sub { font-size: 0.625em; opacity: 0.5; transition: opacity 0.25s ease-in-out; white-space: nowrap; } .filepond--file-info .filepond--file-info-sub:empty { display: none; } .filepond--file-status { position: static; display: flex; flex-direction: column; align-items: flex-end; flex-grow: 0; flex-shrink: 0; margin: 0; min-width: 2.25em; text-align: right; /* will be animated */ will-change: transform, opacity; /* can't do anything with this info */ pointer-events: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; /* no margins on children */ } .filepond--file-status * { margin: 0; white-space: nowrap; } .filepond--file-status { /* font sizes */ } .filepond--file-status .filepond--file-status-main { font-size: 0.75em; line-height: 1.2; } .filepond--file-status .filepond--file-status-sub { font-size: 0.625em; opacity: 0.5; transition: opacity 0.25s ease-in-out; } /* Hard to override styles */ .filepond--file-wrapper.filepond--file-wrapper { border: none; margin: 0; padding: 0; min-width: 0; height: 100%; /* hide legend for visual users */ } .filepond--file-wrapper.filepond--file-wrapper > legend { position: absolute; overflow: hidden; height: 1px; width: 1px; padding: 0; border: 0; clip: rect(1px, 1px, 1px, 1px); -webkit-clip-path: inset(50%); clip-path: inset(50%); white-space: nowrap; } .filepond--file { position: static; display: flex; height: 100%; align-items: flex-start; padding: 0.5625em 0.5625em; color: #fff; border-radius: 0.5em; /* control positions */ } .filepond--file .filepond--file-status { margin-left: auto; margin-right: 2.25em; } .filepond--file .filepond--processing-complete-indicator { pointer-events: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; z-index: 3; } .filepond--file .filepond--processing-complete-indicator, .filepond--file .filepond--progress-indicator, .filepond--file .filepond--file-action-button { position: absolute; } .filepond--file { /* .filepond--file-action-button */ } .filepond--file [data-align*='left'] { left: 0.5625em; } .filepond--file [data-align*='right'] { right: 0.5625em; } .filepond--file [data-align*='center'] { left: calc(50% - 0.8125em); /* .8125 is half of button width */ } .filepond--file [data-align*='bottom'] { bottom: 1.125em; } .filepond--file [data-align='center'] { top: calc(50% - 0.8125em); } .filepond--file .filepond--progress-indicator { margin-top: 0.1875em; } .filepond--file .filepond--progress-indicator[data-align*='right'] { margin-right: 0.1875em; } .filepond--file .filepond--progress-indicator[data-align*='left'] { margin-left: 0.1875em; } /* make sure text does not overlap */ [data-filepond-item-state='cancelled'] .filepond--file-info, [data-filepond-item-state*='invalid'] .filepond--file-info, [data-filepond-item-state*='error'] .filepond--file-info { margin-right: 2.25em; } [data-filepond-item-state~='processing'] .filepond--file-status-sub { opacity: 0; } [data-filepond-item-state~='processing'] .filepond--action-abort-item-processing ~ .filepond--file-status .filepond--file-status-sub { opacity: 0.5; } [data-filepond-item-state='processing-error'] .filepond--file-status-sub { opacity: 0; } [data-filepond-item-state='processing-error'] .filepond--action-retry-item-processing ~ .filepond--file-status .filepond--file-status-sub { opacity: 0.5; } [data-filepond-item-state='processing-complete'] { /* busy state */ } [data-filepond-item-state='processing-complete'] .filepond--action-revert-item-processing svg { -webkit-animation: fall 0.5s 0.125s linear both; animation: fall 0.5s 0.125s linear both; } [data-filepond-item-state='processing-complete'] { /* hide details by default, only show when can revert */ } [data-filepond-item-state='processing-complete'] .filepond--file-status-sub { opacity: 0.5; } [data-filepond-item-state='processing-complete'] .filepond--processing-complete-indicator:not([style*='hidden']) ~ .filepond--file-status .filepond--file-status-sub { opacity: 0; } [data-filepond-item-state='processing-complete'] .filepond--file-info-sub { opacity: 0; } [data-filepond-item-state='processing-complete'] .filepond--action-revert-item-processing ~ .filepond--file-info .filepond--file-info-sub { opacity: 0.5; } /* file state can be invalid or error, both are visually similar but */ /* having them as separate states might be useful */ [data-filepond-item-state*='invalid'] .filepond--panel, [data-filepond-item-state*='invalid'] .filepond--file-wrapper, [data-filepond-item-state*='error'] .filepond--panel, [data-filepond-item-state*='error'] .filepond--file-wrapper { -webkit-animation: shake 0.65s linear both; animation: shake 0.65s linear both; } /* spins progress indicator when file is marked as busy */ [data-filepond-item-state*='busy'] .filepond--progress-indicator svg { -webkit-animation: spin 1s linear infinite; animation: spin 1s linear infinite; } /** * States */ @-webkit-keyframes spin { 0% { -webkit-transform: rotateZ(0deg); transform: rotateZ(0deg); } 100% { -webkit-transform: rotateZ(360deg); transform: rotateZ(360deg); } } @keyframes spin { 0% { -webkit-transform: rotateZ(0deg); transform: rotateZ(0deg); } 100% { -webkit-transform: rotateZ(360deg); transform: rotateZ(360deg); } } @-webkit-keyframes shake { 10%, 90% { -webkit-transform: translateX(-0.0625em); transform: translateX(-0.0625em); } 20%, 80% { -webkit-transform: translateX(0.125em); transform: translateX(0.125em); } 30%, 50%, 70% { -webkit-transform: translateX(-0.25em); transform: translateX(-0.25em); } 40%, 60% { -webkit-transform: translateX(0.25em); transform: translateX(0.25em); } } @keyframes shake { 10%, 90% { -webkit-transform: translateX(-0.0625em); transform: translateX(-0.0625em); } 20%, 80% { -webkit-transform: translateX(0.125em); transform: translateX(0.125em); } 30%, 50%, 70% { -webkit-transform: translateX(-0.25em); transform: translateX(-0.25em); } 40%, 60% { -webkit-transform: translateX(0.25em); transform: translateX(0.25em); } } @-webkit-keyframes fall { 0% { opacity: 0; -webkit-transform: scale(0.5); transform: scale(0.5); -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; } 70% { opacity: 1; -webkit-transform: scale(1.1); transform: scale(1.1); -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out; } 100% { -webkit-transform: scale(1); transform: scale(1); -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; } } @keyframes fall { 0% { opacity: 0; -webkit-transform: scale(0.5); transform: scale(0.5); -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; } 70% { opacity: 1; -webkit-transform: scale(1.1); transform: scale(1.1); -webkit-animation-timing-function: ease-in-out; animation-timing-function: ease-in-out; } 100% { -webkit-transform: scale(1); transform: scale(1); -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; } } /* ignore all other interaction elements while dragging a file */ .filepond--hopper[data-hopper-state='drag-over'] > * { pointer-events: none; } /* capture all hit tests using a hidden layer, this speeds up the event flow */ .filepond--hopper[data-hopper-state='drag-over']::after { content: ''; position: absolute; left: 0; top: 0; right: 0; bottom: 0; z-index: 100; } .filepond--progress-indicator { z-index: 103; } .filepond--file-action-button { z-index: 102; } .filepond--file-status { z-index: 101; } .filepond--file-info { z-index: 100; } .filepond--item { position: absolute; top: 0; left: 0; right: 0; z-index: 1; padding: 0; margin: 0.25em; will-change: transform, opacity; touch-action: auto; /* item children order */ } .filepond--item > .filepond--panel { z-index: -1; } /* has a slight shadow */ .filepond--item > .filepond--panel .filepond--panel-bottom { box-shadow: 0 0.0625em 0.125em -0.0625em rgba(0, 0, 0, 0.25); } .filepond--item { /* drag related */ } .filepond--item > .filepond--file-wrapper, .filepond--item > .filepond--panel { transition: opacity 0.15s ease-out; } .filepond--item[data-drag-state] { cursor: -webkit-grab; cursor: grab; } .filepond--item[data-drag-state] > .filepond--panel { transition: box-shadow 0.125s ease-in-out; box-shadow: 0 0 0 rgba(0, 0, 0, 0); } .filepond--item[data-drag-state='drag'] { cursor: -webkit-grabbing; cursor: grabbing; } .filepond--item[data-drag-state='drag'] > .filepond--panel { box-shadow: 0 0.125em 0.3125em rgba(0, 0, 0, 0.325); } .filepond--item[data-drag-state]:not([data-drag-state='idle']) { z-index: 2; } /* states */ .filepond--item-panel { background-color: #64605e; } [data-filepond-item-state='processing-complete'] .filepond--item-panel { background-color: #369763; } [data-filepond-item-state*='invalid'] .filepond--item-panel, [data-filepond-item-state*='error'] .filepond--item-panel { background-color: #c44e47; } /* style of item panel */ .filepond--item-panel { border-radius: 0.5em; transition: background-color 0.25s; } /* normal mode */ .filepond--list-scroller { position: absolute; top: 0; left: 0; right: 0; margin: 0; will-change: transform; } /* scroll mode */ .filepond--list-scroller[data-state='overflow'] .filepond--list { bottom: 0; right: 0; } .filepond--list-scroller[data-state='overflow'] { overflow-y: scroll; overflow-x: hidden; -webkit-overflow-scrolling: touch; -webkit-mask: linear-gradient(to bottom, #000 calc(100% - 0.5em), transparent 100%); mask: linear-gradient(to bottom, #000 calc(100% - 0.5em), transparent 100%); } /* style scrollbar */ .filepond--list-scroller::-webkit-scrollbar { background: transparent; } .filepond--list-scroller::-webkit-scrollbar:vertical { width: 1em; } .filepond--list-scroller::-webkit-scrollbar:horizontal { height: 0; } .filepond--list-scroller::-webkit-scrollbar-thumb { background-color: rgba(0, 0, 0, 0.3); border-radius: 99999px; border: 0.3125em solid transparent; background-clip: content-box; } /* hard to overide styles on purpose */ .filepond--list.filepond--list { position: absolute; top: 0; margin: 0; padding: 0; list-style-type: none; /* prevents endless paint calls on filepond--list-scroller */ will-change: transform; } /* used for padding so allowed to be restyled */ .filepond--list { left: 0.75em; right: 0.75em; } .filepond--root[data-style-panel-layout~='integrated'] { width: 100%; height: 100%; max-width: none; margin: 0; } .filepond--root[data-style-panel-layout~='circle'] .filepond--panel-root, .filepond--root[data-style-panel-layout~='integrated'] .filepond--panel-root { border-radius: 0; } .filepond--root[data-style-panel-layout~='circle'] .filepond--panel-root > *, .filepond--root[data-style-panel-layout~='integrated'] .filepond--panel-root > * { display: none; } .filepond--root[data-style-panel-layout~='circle'] .filepond--drop-label, .filepond--root[data-style-panel-layout~='integrated'] .filepond--drop-label { bottom: 0; height: auto; display: flex; justify-content: center; align-items: center; z-index: 7; } .filepond--root[data-style-panel-layout~='circle'], .filepond--root[data-style-panel-layout~='integrated'] { /* we're only loading one item, this makes the intro animation a bit nicer */ } .filepond--root[data-style-panel-layout~='circle'] .filepond--item-panel, .filepond--root[data-style-panel-layout~='integrated'] .filepond--item-panel { display: none; } .filepond--root[data-style-panel-layout~='compact'] .filepond--list-scroller, .filepond--root[data-style-panel-layout~='integrated'] .filepond--list-scroller { overflow: hidden; height: 100%; margin-top: 0; margin-bottom: 0; } .filepond--root[data-style-panel-layout~='compact'] .filepond--list, .filepond--root[data-style-panel-layout~='integrated'] .filepond--list { left: 0; right: 0; height: 100%; } .filepond--root[data-style-panel-layout~='compact'] .filepond--item, .filepond--root[data-style-panel-layout~='integrated'] .filepond--item { margin: 0; } .filepond--root[data-style-panel-layout~='compact'] .filepond--file-wrapper, .filepond--root[data-style-panel-layout~='integrated'] .filepond--file-wrapper { height: 100%; } .filepond--root[data-style-panel-layout~='compact'] .filepond--drop-label, .filepond--root[data-style-panel-layout~='integrated'] .filepond--drop-label { z-index: 7; } .filepond--root[data-style-panel-layout~='circle'] { border-radius: 99999rem; overflow: hidden; } .filepond--root[data-style-panel-layout~='circle'] > .filepond--panel { border-radius: inherit; } .filepond--root[data-style-panel-layout~='circle'] > .filepond--panel > * { display: none; } .filepond--root[data-style-panel-layout~='circle'] { /* circle cuts of this info, so best to hide it */ } .filepond--root[data-style-panel-layout~='circle'] .filepond--file-info { display: none; } .filepond--root[data-style-panel-layout~='circle'] .filepond--file-status { display: none; } .filepond--root[data-style-panel-layout~='circle'] .filepond--action-edit-item { opacity: 1 !important; visibility: visible !important; } /* dirfty way to fix circular overflow issue on safari 11+ */ @media not all and (min-resolution: 0.001dpcm) { @supports (-webkit-appearance: none) and (stroke-color: transparent) { .filepond--root[data-style-panel-layout~='circle'] { will-change: transform; } } } .filepond--panel-root { border-radius: 0.5em; background-color: #f1f0ef; } .filepond--panel { position: absolute; left: 0; top: 0; right: 0; margin: 0; /* defaults to 100% height (fixed height mode) this fixes problem with panel height in IE11 */ height: 100% !important; /* no interaction possible with panel */ pointer-events: none; } .filepond-panel:not([data-scalable='false']) { height: auto !important; } .filepond--panel[data-scalable='false'] > div { display: none; } .filepond--panel[data-scalable='true'] { /* this seems to fix Chrome performance issues */ /* - when box-shadow is enabled */ /* - when multiple ponds are active on the same page */ -webkit-transform-style: preserve-3d; transform-style: preserve-3d; /* prevent borders and backgrounds */ background-color: transparent !important; border: none !important; } .filepond--panel-top, .filepond--panel-bottom, .filepond--panel-center { position: absolute; left: 0; top: 0; right: 0; margin: 0; padding: 0; } .filepond--panel-top, .filepond--panel-bottom { height: 0.5em; } .filepond--panel-top { border-bottom-left-radius: 0 !important; border-bottom-right-radius: 0 !important; border-bottom: none !important; /* fixes tiny transparant line between top and center panel */ } .filepond--panel-top::after { content: ''; position: absolute; height: 2px; left: 0; right: 0; bottom: -1px; background-color: inherit; } .filepond--panel-center, .filepond--panel-bottom { will-change: transform; -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-transform-origin: left top; transform-origin: left top; -webkit-transform: translate3d(0, 0.5em, 0); transform: translate3d(0, 0.5em, 0); } .filepond--panel-bottom { border-top-left-radius: 0 !important; border-top-right-radius: 0 !important; border-top: none !important; /* fixes tiny transparant line between bottom and center of panel */ } .filepond--panel-bottom::before { content: ''; position: absolute; height: 2px; left: 0; right: 0; top: -1px; background-color: inherit; } .filepond--panel-center { /* the center panel is scaled using scale3d to fit the correct height */ /* we use 100px instead of 1px as scaling 1px to a huge height is really laggy on chrome */ height: 100px !important; border-top: none !important; border-bottom: none !important; border-radius: 0 !important; /* hide if not transformed, prevents a little flash when the panel is at 100px height while attached for first time */ } .filepond--panel-center:not([style]) { visibility: hidden; } .filepond--progress-indicator { position: static; width: 1.25em; height: 1.25em; color: #fff; /* can't have margins */ margin: 0; /* no interaction possible with progress indicator */ pointer-events: none; /* will be animated */ will-change: transform, opacity; } .filepond--progress-indicator svg { width: 100%; height: 100%; vertical-align: top; transform-box: fill-box; /* should center the animation correctly when zoomed in */ } .filepond--progress-indicator path { fill: none; stroke: currentColor; } .filepond--list-scroller { z-index: 6; } .filepond--drop-label { z-index: 5; } .filepond--drip { z-index: 3; } .filepond--root > .filepond--panel { z-index: 2; } .filepond--browser { z-index: 1; } .filepond--root { /* layout*/ box-sizing: border-box; position: relative; margin-bottom: 1em; /* base font size for whole component */ font-size: 1rem; /* base line height */ line-height: normal; /* up uses default system font family */ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; /* will increase font weight a bit on Safari */ font-weight: 450; /* default text alignment */ text-align: left; /* better text rendering on Safari */ text-rendering: optimizeLegibility; /* text direction is ltr for now */ direction: ltr; /* optimize rendering */ /* https://developer.mozilla.org/en-US/docs/Web/CSS/contain */ contain: layout style size; /* correct box sizing, line-height and positioning on child elements */ } .filepond--root * { box-sizing: inherit; line-height: inherit; } .filepond--root *:not(text) { font-size: inherit; } .filepond--root { /* block everything */ } .filepond--root[data-disabled] { pointer-events: none; } .filepond--root[data-disabled] .filepond--list-scroller { pointer-events: all; } .filepond--root[data-disabled] .filepond--list { pointer-events: none; } /** * Root element children layout */ .filepond--root .filepond--drop-label { min-height: 4.75em; } .filepond--root .filepond--list-scroller { margin-top: 1em; margin-bottom: 1em; } .filepond--root .filepond--credits { position: absolute; right: 0; opacity: 0.4; line-height: 0.85; font-size: 11px; color: inherit; text-decoration: none; z-index: 3; bottom: -14px; } .filepond--root .filepond--credits[style] { top: 0; bottom: auto; margin-top: 14px; } ================================================ FILE: dist/filepond.esm.js ================================================ /*! * FilePond 4.32.12 * Licensed under MIT, https://opensource.org/licenses/MIT/ * Please visit https://pqina.nl/filepond/ for details. */ /* eslint-disable */ const isNode = value => value instanceof HTMLElement; const createStore = (initialState, queries = [], actions = []) => { // internal state const state = { ...initialState, }; // contains all actions for next frame, is clear when actions are requested const actionQueue = []; const dispatchQueue = []; // returns a duplicate of the current state const getState = () => ({ ...state }); // returns a duplicate of the actions array and clears the actions array const processActionQueue = () => { // create copy of actions queue const queue = [...actionQueue]; // clear actions queue (we don't want no double actions) actionQueue.length = 0; return queue; }; // processes actions that might block the main UI thread const processDispatchQueue = () => { // create copy of actions queue const queue = [...dispatchQueue]; // clear actions queue (we don't want no double actions) dispatchQueue.length = 0; // now dispatch these actions queue.forEach(({ type, data }) => { dispatch(type, data); }); }; // adds a new action, calls its handler and const dispatch = (type, data, isBlocking) => { // is blocking action (should never block if document is hidden) if (isBlocking && !document.hidden) { dispatchQueue.push({ type, data }); return; } // if this action has a handler, handle the action if (actionHandlers[type]) { actionHandlers[type](data); } // now add action actionQueue.push({ type, data, }); }; const query = (str, ...args) => (queryHandles[str] ? queryHandles[str](...args) : null); const api = { getState, processActionQueue, processDispatchQueue, dispatch, query, }; let queryHandles = {}; queries.forEach(query => { queryHandles = { ...query(state), ...queryHandles, }; }); let actionHandlers = {}; actions.forEach(action => { actionHandlers = { ...action(dispatch, query, state), ...actionHandlers, }; }); return api; }; const defineProperty = (obj, property, definition) => { if (typeof definition === 'function') { obj[property] = definition; return; } Object.defineProperty(obj, property, { ...definition }); }; const forin = (obj, cb) => { for (const key in obj) { if (!obj.hasOwnProperty(key)) { continue; } cb(key, obj[key]); } }; const createObject = definition => { const obj = {}; forin(definition, property => { defineProperty(obj, property, definition[property]); }); return obj; }; const attr = (node, name, value = null) => { if (value === null) { return node.getAttribute(name) || node.hasAttribute(name); } node.setAttribute(name, value); }; const ns = 'http://www.w3.org/2000/svg'; const svgElements = ['svg', 'path']; // only svg elements used const isSVGElement = tag => svgElements.includes(tag); const createElement = (tag, className, attributes = {}) => { if (typeof className === 'object') { attributes = className; className = null; } const element = isSVGElement(tag) ? document.createElementNS(ns, tag) : document.createElement(tag); if (className) { if (isSVGElement(tag)) { attr(element, 'class', className); } else { element.className = className; } } forin(attributes, (name, value) => { attr(element, name, value); }); return element; }; const appendChild = parent => (child, index) => { if (typeof index !== 'undefined' && parent.children[index]) { parent.insertBefore(child, parent.children[index]); } else { parent.appendChild(child); } }; const appendChildView = (parent, childViews) => (view, index) => { if (typeof index !== 'undefined') { childViews.splice(index, 0, view); } else { childViews.push(view); } return view; }; const removeChildView = (parent, childViews) => view => { // remove from child views childViews.splice(childViews.indexOf(view), 1); // remove the element if (view.element.parentNode) { parent.removeChild(view.element); } return view; }; const IS_BROWSER = (() => typeof window !== 'undefined' && typeof window.document !== 'undefined')(); const isBrowser = () => IS_BROWSER; const testElement = isBrowser() ? createElement('svg') : {}; const getChildCount = 'children' in testElement ? el => el.children.length : el => el.childNodes.length; const getViewRect = (elementRect, childViews, offset, scale) => { const left = offset[0] || elementRect.left; const top = offset[1] || elementRect.top; const right = left + elementRect.width; const bottom = top + elementRect.height * (scale[1] || 1); const rect = { // the rectangle of the element itself element: { ...elementRect, }, // the rectangle of the element expanded to contain its children, does not include any margins inner: { left: elementRect.left, top: elementRect.top, right: elementRect.right, bottom: elementRect.bottom, }, // the rectangle of the element expanded to contain its children including own margin and child margins // margins will be added after we've recalculated the size outer: { left, top, right, bottom, }, }; // expand rect to fit all child rectangles childViews .filter(childView => !childView.isRectIgnored()) .map(childView => childView.rect) .forEach(childViewRect => { expandRect(rect.inner, { ...childViewRect.inner }); expandRect(rect.outer, { ...childViewRect.outer }); }); // calculate inner width and height calculateRectSize(rect.inner); // append additional margin (top and left margins are included in top and left automatically) rect.outer.bottom += rect.element.marginBottom; rect.outer.right += rect.element.marginRight; // calculate outer width and height calculateRectSize(rect.outer); return rect; }; const expandRect = (parent, child) => { // adjust for parent offset child.top += parent.top; child.right += parent.left; child.bottom += parent.top; child.left += parent.left; if (child.bottom > parent.bottom) { parent.bottom = child.bottom; } if (child.right > parent.right) { parent.right = child.right; } }; const calculateRectSize = rect => { rect.width = rect.right - rect.left; rect.height = rect.bottom - rect.top; }; const isNumber = value => typeof value === 'number'; /** * Determines if position is at destination * @param position * @param destination * @param velocity * @param errorMargin * @returns {boolean} */ const thereYet = (position, destination, velocity, errorMargin = 0.001) => { return Math.abs(position - destination) < errorMargin && Math.abs(velocity) < errorMargin; }; /** * Spring animation */ const spring = // default options ({ stiffness = 0.5, damping = 0.75, mass = 10 } = {}) => // method definition { let target = null; let position = null; let velocity = 0; let resting = false; // updates spring state const interpolate = (ts, skipToEndState) => { // in rest, don't animate if (resting) return; // need at least a target or position to do springy things if (!(isNumber(target) && isNumber(position))) { resting = true; velocity = 0; return; } // calculate spring force const f = -(position - target) * stiffness; // update velocity by adding force based on mass velocity += f / mass; // update position by adding velocity position += velocity; // slow down based on amount of damping velocity *= damping; // we've arrived if we're near target and our velocity is near zero if (thereYet(position, target, velocity) || skipToEndState) { position = target; velocity = 0; resting = true; // we done api.onupdate(position); api.oncomplete(position); } else { // progress update api.onupdate(position); } }; /** * Set new target value * @param value */ const setTarget = value => { // if currently has no position, set target and position to this value if (isNumber(value) && !isNumber(position)) { position = value; } // next target value will not be animated to if (target === null) { target = value; position = value; } // let start moving to target target = value; // already at target if (position === target || typeof target === 'undefined') { // now resting as target is current position, stop moving resting = true; velocity = 0; // done! api.onupdate(position); api.oncomplete(position); return; } resting = false; }; // need 'api' to call onupdate callback const api = createObject({ interpolate, target: { set: setTarget, get: () => target, }, resting: { get: () => resting, }, onupdate: value => {}, oncomplete: value => {}, }); return api; }; const easeLinear = t => t; const easeInOutQuad = t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t); const tween = // default values ({ duration = 500, easing = easeInOutQuad, delay = 0 } = {}) => // method definition { let start = null; let t; let p; let resting = true; let reverse = false; let target = null; const interpolate = (ts, skipToEndState) => { if (resting || target === null) return; if (start === null) { start = ts; } if (ts - start < delay) return; t = ts - start - delay; if (t >= duration || skipToEndState) { t = 1; p = reverse ? 0 : 1; api.onupdate(p * target); api.oncomplete(p * target); resting = true; } else { p = t / duration; api.onupdate((t >= 0 ? easing(reverse ? 1 - p : p) : 0) * target); } }; // need 'api' to call onupdate callback const api = createObject({ interpolate, target: { get: () => (reverse ? 0 : target), set: value => { // is initial value if (target === null) { target = value; api.onupdate(value); api.oncomplete(value); return; } // want to tween to a smaller value and have a current value if (value < target) { target = 1; reverse = true; } else { // not tweening to a smaller value reverse = false; target = value; } // let's go! resting = false; start = null; }, }, resting: { get: () => resting, }, onupdate: value => {}, oncomplete: value => {}, }); return api; }; const animator = { spring, tween, }; /* { type: 'spring', stiffness: .5, damping: .75, mass: 10 }; { translation: { type: 'spring', ... }, ... } { translation: { x: { type: 'spring', ... } } } */ const createAnimator = (definition, category, property) => { // default is single definition // we check if transform is set, if so, we check if property is set const def = definition[category] && typeof definition[category][property] === 'object' ? definition[category][property] : definition[category] || definition; const type = typeof def === 'string' ? def : def.type; const props = typeof def === 'object' ? { ...def } : {}; return animator[type] ? animator[type](props) : null; }; const addGetSet = (keys, obj, props, overwrite = false) => { obj = Array.isArray(obj) ? obj : [obj]; obj.forEach(o => { keys.forEach(key => { let name = key; let getter = () => props[key]; let setter = value => (props[key] = value); if (typeof key === 'object') { name = key.key; getter = key.getter || getter; setter = key.setter || setter; } if (o[name] && !overwrite) { return; } o[name] = { get: getter, set: setter, }; }); }); }; // add to state, // add getters and setters to internal and external api (if not set) // setup animators const animations = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI }) => { // initial properties const initialProps = { ...viewProps }; // list of all active animations const animations = []; // setup animators forin(mixinConfig, (property, animation) => { const animator = createAnimator(animation); if (!animator) { return; } // when the animator updates, update the view state value animator.onupdate = value => { viewProps[property] = value; }; // set animator target animator.target = initialProps[property]; // when value is set, set the animator target value const prop = { key: property, setter: value => { // if already at target, we done! if (animator.target === value) { return; } animator.target = value; }, getter: () => viewProps[property], }; // add getters and setters addGetSet([prop], [viewInternalAPI, viewExternalAPI], viewProps, true); // add it to the list for easy updating from the _write method animations.push(animator); }); // expose internal write api return { write: ts => { let skipToEndState = document.hidden; let resting = true; animations.forEach(animation => { if (!animation.resting) resting = false; animation.interpolate(ts, skipToEndState); }); return resting; }, destroy: () => {}, }; }; const addEvent = element => (type, fn) => { element.addEventListener(type, fn); }; const removeEvent = element => (type, fn) => { element.removeEventListener(type, fn); }; // mixin const listeners = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI, viewState, view, }) => { const events = []; const add = addEvent(view.element); const remove = removeEvent(view.element); viewExternalAPI.on = (type, fn) => { events.push({ type, fn, }); add(type, fn); }; viewExternalAPI.off = (type, fn) => { events.splice(events.findIndex(event => event.type === type && event.fn === fn), 1); remove(type, fn); }; return { write: () => { // not busy return true; }, destroy: () => { events.forEach(event => { remove(event.type, event.fn); }); }, }; }; // add to external api and link to props const apis = ({ mixinConfig, viewProps, viewExternalAPI }) => { addGetSet(mixinConfig, viewExternalAPI, viewProps); }; const isDefined = value => value != null; // add to state, // add getters and setters to internal and external api (if not set) // set initial state based on props in viewProps // apply as transforms each frame const defaults = { opacity: 1, scaleX: 1, scaleY: 1, translateX: 0, translateY: 0, rotateX: 0, rotateY: 0, rotateZ: 0, originX: 0, originY: 0, }; const styles = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI, view }) => { // initial props const initialProps = { ...viewProps }; // current props const currentProps = {}; // we will add those properties to the external API and link them to the viewState addGetSet(mixinConfig, [viewInternalAPI, viewExternalAPI], viewProps); // override rect on internal and external rect getter so it takes in account transforms const getOffset = () => [viewProps['translateX'] || 0, viewProps['translateY'] || 0]; const getScale = () => [viewProps['scaleX'] || 0, viewProps['scaleY'] || 0]; const getRect = () => view.rect ? getViewRect(view.rect, view.childViews, getOffset(), getScale()) : null; viewInternalAPI.rect = { get: getRect }; viewExternalAPI.rect = { get: getRect }; // apply view props mixinConfig.forEach(key => { viewProps[key] = typeof initialProps[key] === 'undefined' ? defaults[key] : initialProps[key]; }); // expose api return { write: () => { // see if props have changed if (!propsHaveChanged(currentProps, viewProps)) { return; } // moves element to correct position on screen applyStyles(view.element, viewProps); // store new transforms Object.assign(currentProps, { ...viewProps }); // no longer busy return true; }, destroy: () => {}, }; }; const propsHaveChanged = (currentProps, newProps) => { // different amount of keys if (Object.keys(currentProps).length !== Object.keys(newProps).length) { return true; } // lets analyze the individual props for (const prop in newProps) { if (newProps[prop] !== currentProps[prop]) { return true; } } return false; }; const applyStyles = ( element, { opacity, perspective, translateX, translateY, scaleX, scaleY, rotateX, rotateY, rotateZ, originX, originY, width, height, } ) => { let transforms = ''; let styles = ''; // handle transform origin if (isDefined(originX) || isDefined(originY)) { styles += `transform-origin: ${originX || 0}px ${originY || 0}px;`; } // transform order is relevant // 0. perspective if (isDefined(perspective)) { transforms += `perspective(${perspective}px) `; } // 1. translate if (isDefined(translateX) || isDefined(translateY)) { transforms += `translate3d(${translateX || 0}px, ${translateY || 0}px, 0) `; } // 2. scale if (isDefined(scaleX) || isDefined(scaleY)) { transforms += `scale3d(${isDefined(scaleX) ? scaleX : 1}, ${ isDefined(scaleY) ? scaleY : 1 }, 1) `; } // 3. rotate if (isDefined(rotateZ)) { transforms += `rotateZ(${rotateZ}rad) `; } if (isDefined(rotateX)) { transforms += `rotateX(${rotateX}rad) `; } if (isDefined(rotateY)) { transforms += `rotateY(${rotateY}rad) `; } // add transforms if (transforms.length) { styles += `transform:${transforms};`; } // add opacity if (isDefined(opacity)) { styles += `opacity:${opacity};`; // if we reach zero, we make the element inaccessible if (opacity === 0) { styles += `visibility:hidden;`; } // if we're below 100% opacity this element can't be clicked if (opacity < 1) { styles += `pointer-events:none;`; } } // add height if (isDefined(height)) { styles += `height:${height}px;`; } // add width if (isDefined(width)) { styles += `width:${width}px;`; } // apply styles const elementCurrentStyle = element.elementCurrentStyle || ''; // if new styles does not match current styles, lets update! if (styles.length !== elementCurrentStyle.length || styles !== elementCurrentStyle) { element.style.cssText = styles; // store current styles so we can compare them to new styles later on // _not_ getting the style value is faster element.elementCurrentStyle = styles; } }; const Mixins = { styles, listeners, animations, apis, }; const updateRect = (rect = {}, element = {}, style = {}) => { if (!element.layoutCalculated) { rect.paddingTop = parseInt(style.paddingTop, 10) || 0; rect.marginTop = parseInt(style.marginTop, 10) || 0; rect.marginRight = parseInt(style.marginRight, 10) || 0; rect.marginBottom = parseInt(style.marginBottom, 10) || 0; rect.marginLeft = parseInt(style.marginLeft, 10) || 0; element.layoutCalculated = true; } rect.left = element.offsetLeft || 0; rect.top = element.offsetTop || 0; rect.width = element.offsetWidth || 0; rect.height = element.offsetHeight || 0; rect.right = rect.left + rect.width; rect.bottom = rect.top + rect.height; rect.scrollTop = element.scrollTop; rect.hidden = element.offsetParent === null; return rect; }; const createView = // default view definition ({ // element definition tag = 'div', name = null, attributes = {}, // view interaction read = () => {}, write = () => {}, create = () => {}, destroy = () => {}, // hooks filterFrameActionsForChild = (child, actions) => actions, didCreateView = () => {}, didWriteView = () => {}, // rect related ignoreRect = false, ignoreRectUpdate = false, // mixins mixins = [], } = {}) => ( // each view requires reference to store store, // specific properties for this view props = {} ) => { // root element should not be changed const element = createElement(tag, `filepond--${name}`, attributes); // style reference should also not be changed const style = window.getComputedStyle(element, null); // element rectangle const rect = updateRect(); let frameRect = null; // rest state let isResting = false; // pretty self explanatory const childViews = []; // loaded mixins const activeMixins = []; // references to created children const ref = {}; // state used for each instance const state = {}; // list of writers that will be called to update this view const writers = [ write, // default writer ]; const readers = [ read, // default reader ]; const destroyers = [ destroy, // default destroy ]; // core view methods const getElement = () => element; const getChildViews = () => childViews.concat(); const getReference = () => ref; const createChildView = store => (view, props) => view(store, props); const getRect = () => { if (frameRect) { return frameRect; } frameRect = getViewRect(rect, childViews, [0, 0], [1, 1]); return frameRect; }; const getStyle = () => style; /** * Read data from DOM * @private */ const _read = () => { frameRect = null; // read child views childViews.forEach(child => child._read()); const shouldUpdate = !(ignoreRectUpdate && rect.width && rect.height); if (shouldUpdate) { updateRect(rect, element, style); } // readers const api = { root: internalAPI, props, rect }; readers.forEach(reader => reader(api)); }; /** * Write data to DOM * @private */ const _write = (ts, frameActions, shouldOptimize) => { // if no actions, we assume that the view is resting let resting = frameActions.length === 0; // writers writers.forEach(writer => { const writerResting = writer({ props, root: internalAPI, actions: frameActions, timestamp: ts, shouldOptimize, }); if (writerResting === false) { resting = false; } }); // run mixins activeMixins.forEach(mixin => { // if one of the mixins is still busy after write operation, we are not resting const mixinResting = mixin.write(ts); if (mixinResting === false) { resting = false; } }); // updates child views that are currently attached to the DOM childViews .filter(child => !!child.element.parentNode) .forEach(child => { // if a child view is not resting, we are not resting const childResting = child._write( ts, filterFrameActionsForChild(child, frameActions), shouldOptimize ); if (!childResting) { resting = false; } }); // append new elements to DOM and update those childViews //.filter(child => !child.element.parentNode) .forEach((child, index) => { // skip if (child.element.parentNode) { return; } // append to DOM internalAPI.appendChild(child.element, index); // call read (need to know the size of these elements) child._read(); // re-call write child._write( ts, filterFrameActionsForChild(child, frameActions), shouldOptimize ); // we just added somthing to the dom, no rest resting = false; }); // update resting state isResting = resting; didWriteView({ props, root: internalAPI, actions: frameActions, timestamp: ts, }); // let parent know if we are resting return resting; }; const _destroy = () => { activeMixins.forEach(mixin => mixin.destroy()); destroyers.forEach(destroyer => { destroyer({ root: internalAPI, props }); }); childViews.forEach(child => child._destroy()); }; // sharedAPI const sharedAPIDefinition = { element: { get: getElement, }, style: { get: getStyle, }, childViews: { get: getChildViews, }, }; // private API definition const internalAPIDefinition = { ...sharedAPIDefinition, rect: { get: getRect, }, // access to custom children references ref: { get: getReference, }, // dom modifiers is: needle => name === needle, appendChild: appendChild(element), createChildView: createChildView(store), linkView: view => { childViews.push(view); return view; }, unlinkView: view => { childViews.splice(childViews.indexOf(view), 1); }, appendChildView: appendChildView(element, childViews), removeChildView: removeChildView(element, childViews), registerWriter: writer => writers.push(writer), registerReader: reader => readers.push(reader), registerDestroyer: destroyer => destroyers.push(destroyer), invalidateLayout: () => (element.layoutCalculated = false), // access to data store dispatch: store.dispatch, query: store.query, }; // public view API methods const externalAPIDefinition = { element: { get: getElement, }, childViews: { get: getChildViews, }, rect: { get: getRect, }, resting: { get: () => isResting, }, isRectIgnored: () => ignoreRect, _read, _write, _destroy, }; // mixin API methods const mixinAPIDefinition = { ...sharedAPIDefinition, rect: { get: () => rect, }, }; // add mixin functionality Object.keys(mixins) .sort((a, b) => { // move styles to the back of the mixin list (so adjustments of other mixins are applied to the props correctly) if (a === 'styles') { return 1; } else if (b === 'styles') { return -1; } return 0; }) .forEach(key => { const mixinAPI = Mixins[key]({ mixinConfig: mixins[key], viewProps: props, viewState: state, viewInternalAPI: internalAPIDefinition, viewExternalAPI: externalAPIDefinition, view: createObject(mixinAPIDefinition), }); if (mixinAPI) { activeMixins.push(mixinAPI); } }); // construct private api const internalAPI = createObject(internalAPIDefinition); // create the view create({ root: internalAPI, props, }); // append created child views to root node const childCount = getChildCount(element); // need to know the current child count so appending happens in correct order childViews.forEach((child, index) => { internalAPI.appendChild(child.element, childCount + index); }); // call did create didCreateView(internalAPI); // expose public api return createObject(externalAPIDefinition); }; const createPainter = (read, write, fps = 60) => { const name = '__framePainter'; // set global painter if (window[name]) { window[name].readers.push(read); window[name].writers.push(write); return; } window[name] = { readers: [read], writers: [write], }; const painter = window[name]; const interval = 1000 / fps; let last = null; let id = null; let requestTick = null; let cancelTick = null; const setTimerType = () => { if (document.hidden) { requestTick = () => window.setTimeout(() => tick(performance.now()), interval); cancelTick = () => window.clearTimeout(id); } else { requestTick = () => window.requestAnimationFrame(tick); cancelTick = () => window.cancelAnimationFrame(id); } }; document.addEventListener('visibilitychange', () => { if (cancelTick) cancelTick(); setTimerType(); tick(performance.now()); }); const tick = ts => { // queue next tick id = requestTick(tick); // limit fps if (!last) { last = ts; } const delta = ts - last; if (delta <= interval) { // skip frame return; } // align next frame last = ts - (delta % interval); // update view painter.readers.forEach(read => read()); painter.writers.forEach(write => write(ts)); }; setTimerType(); tick(performance.now()); return { pause: () => { cancelTick(id); }, }; }; const createRoute = (routes, fn) => ({ root, props, actions = [], timestamp, shouldOptimize }) => { actions .filter(action => routes[action.type]) .forEach(action => routes[action.type]({ root, props, action: action.data, timestamp, shouldOptimize }) ); if (fn) { fn({ root, props, actions, timestamp, shouldOptimize }); } }; const insertBefore = (newNode, referenceNode) => referenceNode.parentNode.insertBefore(newNode, referenceNode); const insertAfter = (newNode, referenceNode) => { return referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); }; const isArray = value => Array.isArray(value); const isEmpty = value => value == null; const trim = str => str.trim(); const toString = value => '' + value; const toArray = (value, splitter = ',') => { if (isEmpty(value)) { return []; } if (isArray(value)) { return value; } return toString(value) .split(splitter) .map(trim) .filter(str => str.length); }; const isBoolean = value => typeof value === 'boolean'; const toBoolean = value => (isBoolean(value) ? value : value === 'true'); const isString = value => typeof value === 'string'; const toNumber = value => isNumber(value) ? value : isString(value) ? toString(value).replace(/[a-z]+/gi, '') : 0; const toInt = value => parseInt(toNumber(value), 10); const toFloat = value => parseFloat(toNumber(value)); const isInt = value => isNumber(value) && isFinite(value) && Math.floor(value) === value; const toBytes = (value, base = 1000) => { // is in bytes if (isInt(value)) { return value; } // is natural file size let naturalFileSize = toString(value).trim(); // if is value in megabytes if (/MB$/i.test(naturalFileSize)) { naturalFileSize = naturalFileSize.replace(/MB$i/, '').trim(); return toInt(naturalFileSize) * base * base; } // if is value in kilobytes if (/KB/i.test(naturalFileSize)) { naturalFileSize = naturalFileSize.replace(/KB$i/, '').trim(); return toInt(naturalFileSize) * base; } return toInt(naturalFileSize); }; const isFunction = value => typeof value === 'function'; const toFunctionReference = string => { let ref = self; let levels = string.split('.'); let level = null; while ((level = levels.shift())) { ref = ref[level]; if (!ref) { return null; } } return ref; }; const methods = { process: 'POST', patch: 'PATCH', revert: 'DELETE', fetch: 'GET', restore: 'GET', load: 'GET', }; const createServerAPI = outline => { const api = {}; api.url = isString(outline) ? outline : outline.url || ''; api.timeout = outline.timeout ? parseInt(outline.timeout, 10) : 0; api.headers = outline.headers ? outline.headers : {}; forin(methods, key => { api[key] = createAction(key, outline[key], methods[key], api.timeout, api.headers); }); // remove process if no url or process on outline api.process = outline.process || isString(outline) || outline.url ? api.process : null; // special treatment for remove api.remove = outline.remove || null; // remove generic headers from api object delete api.headers; return api; }; const createAction = (name, outline, method, timeout, headers) => { // is explicitely set to null so disable if (outline === null) { return null; } // if is custom function, done! Dev handles everything. if (typeof outline === 'function') { return outline; } // build action object const action = { url: method === 'GET' || method === 'PATCH' ? `?${name}=` : '', method, headers, withCredentials: false, timeout, onload: null, ondata: null, onerror: null, }; // is a single url if (isString(outline)) { action.url = outline; return action; } // overwrite Object.assign(action, outline); // see if should reformat headers; if (isString(action.headers)) { const parts = action.headers.split(/:(.+)/); action.headers = { header: parts[0], value: parts[1], }; } // if is bool withCredentials action.withCredentials = toBoolean(action.withCredentials); return action; }; const toServerAPI = value => createServerAPI(value); const isNull = value => value === null; const isObject = value => typeof value === 'object' && value !== null; const isAPI = value => { return ( isObject(value) && isString(value.url) && isObject(value.process) && isObject(value.revert) && isObject(value.restore) && isObject(value.fetch) ); }; const getType = value => { if (isArray(value)) { return 'array'; } if (isNull(value)) { return 'null'; } if (isInt(value)) { return 'int'; } if (/^[0-9]+ ?(?:GB|MB|KB)$/gi.test(value)) { return 'bytes'; } if (isAPI(value)) { return 'api'; } return typeof value; }; const replaceSingleQuotes = str => str .replace(/{\s*'/g, '{"') .replace(/'\s*}/g, '"}') .replace(/'\s*:/g, '":') .replace(/:\s*'/g, ':"') .replace(/,\s*'/g, ',"') .replace(/'\s*,/g, '",'); const conversionTable = { array: toArray, boolean: toBoolean, int: value => (getType(value) === 'bytes' ? toBytes(value) : toInt(value)), number: toFloat, float: toFloat, bytes: toBytes, string: value => (isFunction(value) ? value : toString(value)), function: value => toFunctionReference(value), serverapi: toServerAPI, object: value => { try { return JSON.parse(replaceSingleQuotes(value)); } catch (e) { return null; } }, }; const convertTo = (value, type) => conversionTable[type](value); const getValueByType = (newValue, defaultValue, valueType) => { // can always assign default value if (newValue === defaultValue) { return newValue; } // get the type of the new value let newValueType = getType(newValue); // is valid type? if (newValueType !== valueType) { // is string input, let's attempt to convert const convertedValue = convertTo(newValue, valueType); // what is the type now newValueType = getType(convertedValue); // no valid conversions found if (convertedValue === null) { throw `Trying to assign value with incorrect type to "${option}", allowed type: "${valueType}"`; } else { newValue = convertedValue; } } // assign new value return newValue; }; const createOption = (defaultValue, valueType) => { let currentValue = defaultValue; return { enumerable: true, get: () => currentValue, set: newValue => { currentValue = getValueByType(newValue, defaultValue, valueType); }, }; }; const createOptions = options => { const obj = {}; forin(options, prop => { const optionDefinition = options[prop]; obj[prop] = createOption(optionDefinition[0], optionDefinition[1]); }); return createObject(obj); }; const createInitialState = options => ({ // model items: [], // timeout used for calling update items listUpdateTimeout: null, // timeout used for stacking metadata updates itemUpdateTimeout: null, // queue of items waiting to be processed processingQueue: [], // options options: createOptions(options), }); const fromCamels = (string, separator = '-') => string .split(/(?=[A-Z])/) .map(part => part.toLowerCase()) .join(separator); const createOptionAPI = (store, options) => { const obj = {}; forin(options, key => { obj[key] = { get: () => store.getState().options[key], set: value => { store.dispatch(`SET_${fromCamels(key, '_').toUpperCase()}`, { value, }); }, }; }); return obj; }; const createOptionActions = options => (dispatch, query, state) => { const obj = {}; forin(options, key => { const name = fromCamels(key, '_').toUpperCase(); obj[`SET_${name}`] = action => { try { state.options[key] = action.value; } catch (e) { // nope, failed } // we successfully set the value of this option dispatch(`DID_SET_${name}`, { value: state.options[key] }); }; }); return obj; }; const createOptionQueries = options => state => { const obj = {}; forin(options, key => { obj[`GET_${fromCamels(key, '_').toUpperCase()}`] = action => state.options[key]; }); return obj; }; const InteractionMethod = { API: 1, DROP: 2, BROWSE: 3, PASTE: 4, NONE: 5, }; const getUniqueId = () => Math.random() .toString(36) .substring(2, 11); const arrayRemove = (arr, index) => arr.splice(index, 1); const run = (cb, sync) => { if (sync) { cb(); } else if (document.hidden) { Promise.resolve(1).then(cb); } else { setTimeout(cb, 0); } }; const on = () => { const listeners = []; const off = (event, cb) => { arrayRemove( listeners, listeners.findIndex(listener => listener.event === event && (listener.cb === cb || !cb)) ); }; const fire = (event, args, sync) => { listeners .filter(listener => listener.event === event) .map(listener => listener.cb) .forEach(cb => run(() => cb(...args), sync)); }; return { fireSync: (event, ...args) => { fire(event, args, true); }, fire: (event, ...args) => { fire(event, args, false); }, on: (event, cb) => { listeners.push({ event, cb }); }, onOnce: (event, cb) => { listeners.push({ event, cb: (...args) => { off(event, cb); cb(...args); }, }); }, off, }; }; const copyObjectPropertiesToObject = (src, target, excluded) => { Object.getOwnPropertyNames(src) .filter(property => !excluded.includes(property)) .forEach(key => Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(src, key)) ); }; const PRIVATE = [ 'fire', 'process', 'revert', 'load', 'on', 'off', 'onOnce', 'retryLoad', 'extend', 'archive', 'archived', 'release', 'released', 'requestProcessing', 'freeze', ]; const createItemAPI = item => { const api = {}; copyObjectPropertiesToObject(item, api, PRIVATE); return api; }; const removeReleasedItems = items => { items.forEach((item, index) => { if (item.released) { arrayRemove(items, index); } }); }; const ItemStatus = { INIT: 1, IDLE: 2, PROCESSING_QUEUED: 9, PROCESSING: 3, PROCESSING_COMPLETE: 5, PROCESSING_ERROR: 6, PROCESSING_REVERT_ERROR: 10, LOADING: 7, LOAD_ERROR: 8, }; const FileOrigin = { INPUT: 1, LIMBO: 2, LOCAL: 3, }; const getNonNumeric = str => /[^0-9]+/.exec(str); const getDecimalSeparator = () => getNonNumeric((1.1).toLocaleString())[0]; const getThousandsSeparator = () => { // Added for browsers that do not return the thousands separator (happend on native browser Android 4.4.4) // We check against the normal toString output and if they're the same return a comma when decimal separator is a dot const decimalSeparator = getDecimalSeparator(); const thousandsStringWithSeparator = (1000.0).toLocaleString(); const thousandsStringWithoutSeparator = (1000.0).toString(); if (thousandsStringWithSeparator !== thousandsStringWithoutSeparator) { return getNonNumeric(thousandsStringWithSeparator)[0]; } return decimalSeparator === '.' ? ',' : '.'; }; const Type = { BOOLEAN: 'boolean', INT: 'int', NUMBER: 'number', STRING: 'string', ARRAY: 'array', OBJECT: 'object', FUNCTION: 'function', ACTION: 'action', SERVER_API: 'serverapi', REGEX: 'regex', }; // all registered filters const filters = []; // loops over matching filters and passes options to each filter, returning the mapped results const applyFilterChain = (key, value, utils) => new Promise((resolve, reject) => { // find matching filters for this key const matchingFilters = filters.filter(f => f.key === key).map(f => f.cb); // resolve now if (matchingFilters.length === 0) { resolve(value); return; } // first filter to kick things of const initialFilter = matchingFilters.shift(); // chain filters matchingFilters .reduce( // loop over promises passing value to next promise (current, next) => current.then(value => next(value, utils)), // call initial filter, will return a promise initialFilter(value, utils) // all executed ) .then(value => resolve(value)) .catch(error => reject(error)); }); const applyFilters = (key, value, utils) => filters.filter(f => f.key === key).map(f => f.cb(value, utils)); // adds a new filter to the list const addFilter = (key, cb) => filters.push({ key, cb }); const extendDefaultOptions = additionalOptions => Object.assign(defaultOptions, additionalOptions); const getOptions = () => ({ ...defaultOptions }); const setOptions = opts => { forin(opts, (key, value) => { // key does not exist, so this option cannot be set if (!defaultOptions[key]) { return; } defaultOptions[key][0] = getValueByType( value, defaultOptions[key][0], defaultOptions[key][1] ); }); }; // default options on app const defaultOptions = { // the id to add to the root element id: [null, Type.STRING], // input field name to use name: ['filepond', Type.STRING], // disable the field disabled: [false, Type.BOOLEAN], // classname to put on wrapper className: [null, Type.STRING], // is the field required required: [false, Type.BOOLEAN], // Allow media capture when value is set captureMethod: [null, Type.STRING], // - "camera", "microphone" or "camcorder", // - Does not work with multiple on apple devices // - If set, acceptedFileTypes must be made to match with media wildcard "image/*", "audio/*" or "video/*" // sync `acceptedFileTypes` property with `accept` attribute allowSyncAcceptAttribute: [true, Type.BOOLEAN], // Feature toggles allowDrop: [true, Type.BOOLEAN], // Allow dropping of files allowBrowse: [true, Type.BOOLEAN], // Allow browsing the file system allowPaste: [true, Type.BOOLEAN], // Allow pasting files allowMultiple: [false, Type.BOOLEAN], // Allow multiple files (disabled by default, as multiple attribute is also required on input to allow multiple) allowReplace: [true, Type.BOOLEAN], // Allow dropping a file on other file to replace it (only works when multiple is set to false) allowRevert: [true, Type.BOOLEAN], // Allows user to revert file upload allowRemove: [true, Type.BOOLEAN], // Allow user to remove a file allowProcess: [true, Type.BOOLEAN], // Allows user to process a file, when set to false, this removes the file upload button allowReorder: [false, Type.BOOLEAN], // Allow reordering of files allowDirectoriesOnly: [false, Type.BOOLEAN], // Allow only selecting directories with browse (no support for filtering dnd at this point) // Try store file if `server` not set storeAsFile: [false, Type.BOOLEAN], // Revert mode forceRevert: [false, Type.BOOLEAN], // Set to 'force' to require the file to be reverted before removal // Input requirements maxFiles: [null, Type.INT], // Max number of files checkValidity: [false, Type.BOOLEAN], // Enables custom validity messages // Where to put file itemInsertLocationFreedom: [true, Type.BOOLEAN], // Set to false to always add items to begin or end of list itemInsertLocation: ['before', Type.STRING], // Default index in list to add items that have been dropped at the top of the list itemInsertInterval: [75, Type.INT], // Drag 'n Drop related dropOnPage: [false, Type.BOOLEAN], // Allow dropping of files anywhere on page (prevents browser from opening file if dropped outside of Up) dropOnElement: [true, Type.BOOLEAN], // Drop needs to happen on element (set to false to also load drops outside of Up) dropValidation: [false, Type.BOOLEAN], // Enable or disable validating files on drop ignoredFiles: [['.ds_store', 'thumbs.db', 'desktop.ini'], Type.ARRAY], // Upload related instantUpload: [true, Type.BOOLEAN], // Should upload files immediately on drop maxParallelUploads: [2, Type.INT], // Maximum files to upload in parallel allowMinimumUploadDuration: [true, Type.BOOLEAN], // if true uploads take at least 750 ms, this ensures the user sees the upload progress giving trust the upload actually happened // Chunks chunkUploads: [false, Type.BOOLEAN], // Enable chunked uploads chunkForce: [false, Type.BOOLEAN], // Force use of chunk uploads even for files smaller than chunk size chunkSize: [5000000, Type.INT], // Size of chunks (5MB default) chunkRetryDelays: [[500, 1000, 3000], Type.ARRAY], // Amount of times to retry upload of a chunk when it fails // The server api end points to use for uploading (see docs) server: [null, Type.SERVER_API], // File size calculations, can set to 1024, this is only used for display, properties use file size base 1000 fileSizeBase: [1000, Type.INT], // Labels and status messages labelFileSizeBytes: ['bytes', Type.STRING], labelFileSizeKilobytes: ['KB', Type.STRING], labelFileSizeMegabytes: ['MB', Type.STRING], labelFileSizeGigabytes: ['GB', Type.STRING], labelDecimalSeparator: [getDecimalSeparator(), Type.STRING], // Default is locale separator labelThousandsSeparator: [getThousandsSeparator(), Type.STRING], // Default is locale separator labelIdle: [ 'Drag & Drop your files or Browse', Type.STRING, ], labelInvalidField: ['Field contains invalid files', Type.STRING], labelFileWaitingForSize: ['Waiting for size', Type.STRING], labelFileSizeNotAvailable: ['Size not available', Type.STRING], labelFileCountSingular: ['file in list', Type.STRING], labelFileCountPlural: ['files in list', Type.STRING], labelFileLoading: ['Loading', Type.STRING], labelFileAdded: ['Added', Type.STRING], // assistive only labelFileLoadError: ['Error during load', Type.STRING], labelFileRemoved: ['Removed', Type.STRING], // assistive only labelFileRemoveError: ['Error during remove', Type.STRING], labelFileProcessing: ['Uploading', Type.STRING], labelFileProcessingComplete: ['Upload complete', Type.STRING], labelFileProcessingAborted: ['Upload cancelled', Type.STRING], labelFileProcessingError: ['Error during upload', Type.STRING], labelFileProcessingRevertError: ['Error during revert', Type.STRING], labelTapToCancel: ['tap to cancel', Type.STRING], labelTapToRetry: ['tap to retry', Type.STRING], labelTapToUndo: ['tap to undo', Type.STRING], labelButtonRemoveItem: ['Remove', Type.STRING], labelButtonAbortItemLoad: ['Abort', Type.STRING], labelButtonRetryItemLoad: ['Retry', Type.STRING], labelButtonAbortItemProcessing: ['Cancel', Type.STRING], labelButtonUndoItemProcessing: ['Undo', Type.STRING], labelButtonRetryItemProcessing: ['Retry', Type.STRING], labelButtonProcessItem: ['Upload', Type.STRING], // make sure width and height plus viewpox are even numbers so icons are nicely centered iconRemove: [ '', Type.STRING, ], iconProcess: [ '', Type.STRING, ], iconRetry: [ '', Type.STRING, ], iconUndo: [ '', Type.STRING, ], iconDone: [ '', Type.STRING, ], // event handlers oninit: [null, Type.FUNCTION], onwarning: [null, Type.FUNCTION], onerror: [null, Type.FUNCTION], onactivatefile: [null, Type.FUNCTION], oninitfile: [null, Type.FUNCTION], onaddfilestart: [null, Type.FUNCTION], onaddfileprogress: [null, Type.FUNCTION], onaddfile: [null, Type.FUNCTION], onprocessfilestart: [null, Type.FUNCTION], onprocessfileprogress: [null, Type.FUNCTION], onprocessfileabort: [null, Type.FUNCTION], onprocessfilerevert: [null, Type.FUNCTION], onprocessfile: [null, Type.FUNCTION], onprocessfiles: [null, Type.FUNCTION], onremovefile: [null, Type.FUNCTION], onpreparefile: [null, Type.FUNCTION], onupdatefiles: [null, Type.FUNCTION], onreorderfiles: [null, Type.FUNCTION], // hooks beforeDropFile: [null, Type.FUNCTION], beforeAddFile: [null, Type.FUNCTION], beforeRemoveFile: [null, Type.FUNCTION], beforePrepareFile: [null, Type.FUNCTION], // styles stylePanelLayout: [null, Type.STRING], // null 'integrated', 'compact', 'circle' stylePanelAspectRatio: [null, Type.STRING], // null or '3:2' or 1 styleItemPanelAspectRatio: [null, Type.STRING], styleButtonRemoveItemPosition: ['left', Type.STRING], styleButtonProcessItemPosition: ['right', Type.STRING], styleLoadIndicatorPosition: ['right', Type.STRING], styleProgressIndicatorPosition: ['right', Type.STRING], styleButtonRemoveItemAlign: [false, Type.BOOLEAN], // custom initial files array files: [[], Type.ARRAY], // show support by displaying credits credits: [['https://filepond.com', 'Powered by FilePond'], Type.ARRAY], }; const getItemByQuery = (items, query) => { // just return first index if (isEmpty(query)) { return items[0] || null; } // query is index if (isInt(query)) { return items[query] || null; } // if query is item, get the id if (typeof query === 'object') { query = query.id; } // assume query is a string and return item by id return items.find(item => item.id === query) || null; }; const getNumericAspectRatioFromString = aspectRatio => { if (isEmpty(aspectRatio)) { return aspectRatio; } if (/:/.test(aspectRatio)) { const parts = aspectRatio.split(':'); return parts[1] / parts[0]; } return parseFloat(aspectRatio); }; const getActiveItems = items => items.filter(item => !item.archived); const Status = { EMPTY: 0, IDLE: 1, // waiting ERROR: 2, // a file is in error state BUSY: 3, // busy processing or loading READY: 4, // all files uploaded }; let res = null; const canUpdateFileInput = () => { if (res === null) { try { const dataTransfer = new DataTransfer(); dataTransfer.items.add(new File(['hello world'], 'This_Works.txt')); const el = document.createElement('input'); el.setAttribute('type', 'file'); el.files = dataTransfer.files; res = el.files.length === 1; } catch (err) { res = false; } } return res; }; const ITEM_ERROR = [ ItemStatus.LOAD_ERROR, ItemStatus.PROCESSING_ERROR, ItemStatus.PROCESSING_REVERT_ERROR, ]; const ITEM_BUSY = [ ItemStatus.LOADING, ItemStatus.PROCESSING, ItemStatus.PROCESSING_QUEUED, ItemStatus.INIT, ]; const ITEM_READY = [ItemStatus.PROCESSING_COMPLETE]; const isItemInErrorState = item => ITEM_ERROR.includes(item.status); const isItemInBusyState = item => ITEM_BUSY.includes(item.status); const isItemInReadyState = item => ITEM_READY.includes(item.status); const isAsync = state => isObject(state.options.server) && (isObject(state.options.server.process) || isFunction(state.options.server.process)); const queries = state => ({ GET_STATUS: () => { const items = getActiveItems(state.items); const { EMPTY, ERROR, BUSY, IDLE, READY } = Status; if (items.length === 0) return EMPTY; if (items.some(isItemInErrorState)) return ERROR; if (items.some(isItemInBusyState)) return BUSY; if (items.some(isItemInReadyState)) return READY; return IDLE; }, GET_ITEM: query => getItemByQuery(state.items, query), GET_ACTIVE_ITEM: query => getItemByQuery(getActiveItems(state.items), query), GET_ACTIVE_ITEMS: () => getActiveItems(state.items), GET_ITEMS: () => state.items, GET_ITEM_NAME: query => { const item = getItemByQuery(state.items, query); return item ? item.filename : null; }, GET_ITEM_SIZE: query => { const item = getItemByQuery(state.items, query); return item ? item.fileSize : null; }, GET_STYLES: () => Object.keys(state.options) .filter(key => /^style/.test(key)) .map(option => ({ name: option, value: state.options[option], })), GET_PANEL_ASPECT_RATIO: () => { const isShapeCircle = /circle/.test(state.options.stylePanelLayout); const aspectRatio = isShapeCircle ? 1 : getNumericAspectRatioFromString(state.options.stylePanelAspectRatio); return aspectRatio; }, GET_ITEM_PANEL_ASPECT_RATIO: () => state.options.styleItemPanelAspectRatio, GET_ITEMS_BY_STATUS: status => getActiveItems(state.items).filter(item => item.status === status), GET_TOTAL_ITEMS: () => getActiveItems(state.items).length, SHOULD_UPDATE_FILE_INPUT: () => state.options.storeAsFile && canUpdateFileInput() && !isAsync(state), IS_ASYNC: () => isAsync(state), GET_FILE_SIZE_LABELS: query => ({ labelBytes: query('GET_LABEL_FILE_SIZE_BYTES') || undefined, labelKilobytes: query('GET_LABEL_FILE_SIZE_KILOBYTES') || undefined, labelMegabytes: query('GET_LABEL_FILE_SIZE_MEGABYTES') || undefined, labelGigabytes: query('GET_LABEL_FILE_SIZE_GIGABYTES') || undefined, }), }); const hasRoomForItem = state => { const count = getActiveItems(state.items).length; // if cannot have multiple items, to add one item it should currently not contain items if (!state.options.allowMultiple) { return count === 0; } // if allows multiple items, we check if a max item count has been set, if not, there's no limit const maxFileCount = state.options.maxFiles; if (maxFileCount === null) { return true; } // we check if the current count is smaller than the max count, if so, another file can still be added if (count < maxFileCount) { return true; } // no more room for another file return false; }; const limit = (value, min, max) => Math.max(Math.min(max, value), min); const arrayInsert = (arr, index, item) => arr.splice(index, 0, item); const insertItem = (items, item, index) => { if (isEmpty(item)) { return null; } // if index is undefined, append if (typeof index === 'undefined') { items.push(item); return item; } // limit the index to the size of the items array index = limit(index, 0, items.length); // add item to array arrayInsert(items, index, item); // expose return item; }; const isBase64DataURI = str => /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*)\s*$/i.test( str ); const getFilenameFromURL = url => `${url}` .split('/') .pop() .split('?') .shift(); const getExtensionFromFilename = name => name.split('.').pop(); const guesstimateExtension = type => { // if no extension supplied, exit here if (typeof type !== 'string') { return ''; } // get subtype const subtype = type.split('/').pop(); // is svg subtype if (/svg/.test(subtype)) { return 'svg'; } if (/zip|compressed/.test(subtype)) { return 'zip'; } if (/plain/.test(subtype)) { return 'txt'; } if (/msword/.test(subtype)) { return 'doc'; } // if is valid subtype if (/[a-z]+/.test(subtype)) { // always use jpg extension if (subtype === 'jpeg') { return 'jpg'; } // return subtype return subtype; } return ''; }; const leftPad = (value, padding = '') => (padding + value).slice(-padding.length); const getDateString = (date = new Date()) => `${date.getFullYear()}-${leftPad(date.getMonth() + 1, '00')}-${leftPad( date.getDate(), '00' )}_${leftPad(date.getHours(), '00')}-${leftPad(date.getMinutes(), '00')}-${leftPad( date.getSeconds(), '00' )}`; const getFileFromBlob = (blob, filename, type = null, extension = null) => { const file = typeof type === 'string' ? blob.slice(0, blob.size, type) : blob.slice(0, blob.size, blob.type); file.lastModifiedDate = new Date(); // copy relative path if (blob._relativePath) file._relativePath = blob._relativePath; // if blob has name property, use as filename if no filename supplied if (!isString(filename)) { filename = getDateString(); } // if filename supplied but no extension and filename has extension if (filename && extension === null && getExtensionFromFilename(filename)) { file.name = filename; } else { extension = extension || guesstimateExtension(file.type); file.name = filename + (extension ? '.' + extension : ''); } return file; }; const getBlobBuilder = () => { return (window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder); }; const createBlob = (arrayBuffer, mimeType) => { const BB = getBlobBuilder(); if (BB) { const bb = new BB(); bb.append(arrayBuffer); return bb.getBlob(mimeType); } return new Blob([arrayBuffer], { type: mimeType, }); }; const getBlobFromByteStringWithMimeType = (byteString, mimeType) => { const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(ab); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } return createBlob(ab, mimeType); }; const getMimeTypeFromBase64DataURI = dataURI => { return (/^data:(.+);/.exec(dataURI) || [])[1] || null; }; const getBase64DataFromBase64DataURI = dataURI => { // get data part of string (remove data:image/jpeg...,) const data = dataURI.split(',')[1]; // remove any whitespace as that causes InvalidCharacterError in IE return data.replace(/\s/g, ''); }; const getByteStringFromBase64DataURI = dataURI => { return atob(getBase64DataFromBase64DataURI(dataURI)); }; const getBlobFromBase64DataURI = dataURI => { const mimeType = getMimeTypeFromBase64DataURI(dataURI); const byteString = getByteStringFromBase64DataURI(dataURI); return getBlobFromByteStringWithMimeType(byteString, mimeType); }; const getFileFromBase64DataURI = (dataURI, filename, extension) => { return getFileFromBlob(getBlobFromBase64DataURI(dataURI), filename, null, extension); }; const getFileNameFromHeader = header => { // test if is content disposition header, if not exit if (!/^content-disposition:/i.test(header)) return null; // get filename parts const matches = header .split(/filename=|filename\*=.+''/) .splice(1) .map(name => name.trim().replace(/^["']|[;"']{0,2}$/g, '')) .filter(name => name.length); return matches.length ? decodeURI(matches[matches.length - 1]) : null; }; const getFileSizeFromHeader = header => { if (/content-length:/i.test(header)) { const size = header.match(/[0-9]+/)[0]; return size ? parseInt(size, 10) : null; } return null; }; const getTranfserIdFromHeader = header => { if (/x-content-transfer-id:/i.test(header)) { const id = (header.split(':')[1] || '').trim(); return id || null; } return null; }; const getFileInfoFromHeaders = headers => { const info = { source: null, name: null, size: null, }; const rows = headers.split('\n'); for (let header of rows) { const name = getFileNameFromHeader(header); if (name) { info.name = name; continue; } const size = getFileSizeFromHeader(header); if (size) { info.size = size; continue; } const source = getTranfserIdFromHeader(header); if (source) { info.source = source; continue; } } return info; }; const createFileLoader = fetchFn => { const state = { source: null, complete: false, progress: 0, size: null, timestamp: null, duration: 0, request: null, }; const getProgress = () => state.progress; const abort = () => { if (state.request && state.request.abort) { state.request.abort(); } }; // load source const load = () => { // get quick reference const source = state.source; api.fire('init', source); // Load Files if (source instanceof File) { api.fire('load', source); } else if (source instanceof Blob) { // Load blobs, set default name to current date api.fire('load', getFileFromBlob(source, source.name)); } else if (isBase64DataURI(source)) { // Load base 64, set default name to current date api.fire('load', getFileFromBase64DataURI(source)); } else { // Deal as if is external URL, let's load it! loadURL(source); } }; // loads a url const loadURL = url => { // is remote url and no fetch method supplied if (!fetchFn) { api.fire('error', { type: 'error', body: "Can't load URL", code: 400, }); return; } // set request start state.timestamp = Date.now(); // load file state.request = fetchFn( url, response => { // update duration state.duration = Date.now() - state.timestamp; // done! state.complete = true; // turn blob response into a file if (response instanceof Blob) { response = getFileFromBlob(response, response.name || getFilenameFromURL(url)); } api.fire( 'load', // if has received blob, we go with blob, if no response, we return null response instanceof Blob ? response : response ? response.body : null ); }, error => { api.fire( 'error', typeof error === 'string' ? { type: 'error', code: 0, body: error, } : error ); }, (computable, current, total) => { // collected some meta data already if (total) { state.size = total; } // update duration state.duration = Date.now() - state.timestamp; // if we can't compute progress, we're not going to fire progress events if (!computable) { state.progress = null; return; } // update progress percentage state.progress = current / total; // expose api.fire('progress', state.progress); }, () => { api.fire('abort'); }, response => { const fileinfo = getFileInfoFromHeaders( typeof response === 'string' ? response : response.headers ); api.fire('meta', { size: state.size || fileinfo.size, filename: fileinfo.name, source: fileinfo.source, }); } ); }; const api = { ...on(), setSource: source => (state.source = source), getProgress, // file load progress abort, // abort file load load, // start load }; return api; }; const isGet = method => /GET|HEAD/.test(method); const sendRequest = (data, url, options) => { const api = { onheaders: () => {}, onprogress: () => {}, onload: () => {}, ontimeout: () => {}, onerror: () => {}, onabort: () => {}, abort: () => { aborted = true; xhr.abort(); }, }; // timeout identifier, only used when timeout is defined let aborted = false; let headersReceived = false; // set default options options = { method: 'POST', headers: {}, withCredentials: false, ...options, }; // encode url url = encodeURI(url); // if method is GET, add any received data to url if (isGet(options.method) && data) { url = `${url}${encodeURIComponent(typeof data === 'string' ? data : JSON.stringify(data))}`; } // create request const xhr = new XMLHttpRequest(); // progress of load const process = isGet(options.method) ? xhr : xhr.upload; process.onprogress = e => { // no progress event when aborted ( onprogress is called once after abort() ) if (aborted) { return; } api.onprogress(e.lengthComputable, e.loaded, e.total); }; // tries to get header info to the app as fast as possible xhr.onreadystatechange = () => { // not interesting in these states ('unsent' and 'openend' as they don't give us any additional info) if (xhr.readyState < 2) { return; } // no server response if (xhr.readyState === 4 && xhr.status === 0) { return; } if (headersReceived) { return; } headersReceived = true; // we've probably received some useful data in response headers api.onheaders(xhr); }; // load successful xhr.onload = () => { // is classified as valid response if (xhr.status >= 200 && xhr.status < 300) { api.onload(xhr); } else { api.onerror(xhr); } }; // error during load xhr.onerror = () => api.onerror(xhr); // request aborted xhr.onabort = () => { aborted = true; api.onabort(); }; // request timeout xhr.ontimeout = () => api.ontimeout(xhr); // open up open up! xhr.open(options.method, url, true); // set timeout if defined (do it after open so IE11 plays ball) if (isInt(options.timeout)) { xhr.timeout = options.timeout; } // add headers Object.keys(options.headers).forEach(key => { const value = unescape(encodeURIComponent(options.headers[key])); xhr.setRequestHeader(key, value); }); // set type of response if (options.responseType) { xhr.responseType = options.responseType; } // set credentials if (options.withCredentials) { xhr.withCredentials = true; } // let's send our data xhr.send(data); return api; }; const createResponse = (type, code, body, headers) => ({ type, code, body, headers, }); const createTimeoutResponse = cb => xhr => { cb(createResponse('error', 0, 'Timeout', xhr.getAllResponseHeaders())); }; const hasQS = str => /\?/.test(str); const buildURL = (...parts) => { let url = ''; parts.forEach(part => { url += hasQS(url) && hasQS(part) ? part.replace(/\?/, '&') : part; }); return url; }; const createFetchFunction = (apiUrl = '', action) => { // custom handler (should also handle file, load, error, progress and abort) if (typeof action === 'function') { return action; } // no action supplied if (!action || !isString(action.url)) { return null; } // set onload hanlder const onload = action.onload || (res => res); const onerror = action.onerror || (res => null); // internal handler return (url, load, error, progress, abort, headers) => { // do local or remote request based on if the url is external const request = sendRequest(url, buildURL(apiUrl, action.url), { ...action, responseType: 'blob', }); request.onload = xhr => { // get headers const headers = xhr.getAllResponseHeaders(); // get filename const filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url); // create response load( createResponse( 'load', xhr.status, action.method === 'HEAD' ? null : getFileFromBlob(onload(xhr.response), filename), headers ) ); }; request.onerror = xhr => { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.onheaders = xhr => { headers(createResponse('headers', xhr.status, null, xhr.getAllResponseHeaders())); }; request.ontimeout = createTimeoutResponse(error); request.onprogress = progress; request.onabort = abort; // should return request return request; }; }; const ChunkStatus = { QUEUED: 0, COMPLETE: 1, PROCESSING: 2, ERROR: 3, WAITING: 4, }; /* function signature: (file, metadata, load, error, progress, abort, transfer, options) => { return { abort:() => {} } } */ // apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options const processFileChunked = ( apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options ) => { // all chunks const chunks = []; const { chunkTransferId, chunkServer, chunkSize, chunkRetryDelays } = options; // default state const state = { serverId: chunkTransferId, aborted: false, }; // set onload handlers const ondata = action.ondata || (fd => fd); const onload = action.onload || ((xhr, method) => method === 'HEAD' ? xhr.getResponseHeader('Upload-Offset') : xhr.response); const onerror = action.onerror || (res => null); // create server hook const requestTransferId = cb => { const formData = new FormData(); // add metadata under same name if (isObject(metadata)) formData.append(name, JSON.stringify(metadata)); const headers = typeof action.headers === 'function' ? action.headers(file, metadata) : { ...action.headers, 'Upload-Length': file.size, }; const requestParams = { ...action, headers, }; // send request object const request = sendRequest(ondata(formData), buildURL(apiUrl, action.url), requestParams); request.onload = xhr => cb(onload(xhr, requestParams.method)); request.onerror = xhr => error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); request.ontimeout = createTimeoutResponse(error); }; const requestTransferOffset = cb => { const requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId); const headers = typeof action.headers === 'function' ? action.headers(state.serverId) : { ...action.headers, }; const requestParams = { headers, method: 'HEAD', }; const request = sendRequest(null, requestUrl, requestParams); request.onload = xhr => cb(onload(xhr, requestParams.method)); request.onerror = xhr => error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); request.ontimeout = createTimeoutResponse(error); }; // create chunks const lastChunkIndex = Math.floor(file.size / chunkSize); for (let i = 0; i <= lastChunkIndex; i++) { const offset = i * chunkSize; const data = file.slice(offset, offset + chunkSize, 'application/offset+octet-stream'); chunks[i] = { index: i, size: data.size, offset, data, file, progress: 0, retries: [...chunkRetryDelays], status: ChunkStatus.QUEUED, error: null, request: null, timeout: null, }; } const completeProcessingChunks = () => load(state.serverId); const canProcessChunk = chunk => chunk.status === ChunkStatus.QUEUED || chunk.status === ChunkStatus.ERROR; const processChunk = chunk => { // processing is paused, wait here if (state.aborted) return; // get next chunk to process chunk = chunk || chunks.find(canProcessChunk); // no more chunks to process if (!chunk) { // all done? if (chunks.every(chunk => chunk.status === ChunkStatus.COMPLETE)) { completeProcessingChunks(); } // no chunk to handle return; } // now processing this chunk chunk.status = ChunkStatus.PROCESSING; chunk.progress = null; // allow parsing of formdata const ondata = chunkServer.ondata || (fd => fd); const onerror = chunkServer.onerror || (res => null); const onload = chunkServer.onload || (() => {}); // send request object const requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId); const headers = typeof chunkServer.headers === 'function' ? chunkServer.headers(chunk) : { ...chunkServer.headers, 'Content-Type': 'application/offset+octet-stream', 'Upload-Offset': chunk.offset, 'Upload-Length': file.size, 'Upload-Name': file.name, }; const request = (chunk.request = sendRequest(ondata(chunk.data), requestUrl, { ...chunkServer, headers, })); request.onload = xhr => { // allow hooking into request result onload(xhr, chunk.index, chunks.length); // done! chunk.status = ChunkStatus.COMPLETE; // remove request reference chunk.request = null; // start processing more chunks processChunks(); }; request.onprogress = (lengthComputable, loaded, total) => { chunk.progress = lengthComputable ? loaded : null; updateTotalProgress(); }; request.onerror = xhr => { chunk.status = ChunkStatus.ERROR; chunk.request = null; chunk.error = onerror(xhr.response) || xhr.statusText; if (!retryProcessChunk(chunk)) { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); } }; request.ontimeout = xhr => { chunk.status = ChunkStatus.ERROR; chunk.request = null; if (!retryProcessChunk(chunk)) { createTimeoutResponse(error)(xhr); } }; request.onabort = () => { chunk.status = ChunkStatus.QUEUED; chunk.request = null; abort(); }; }; const retryProcessChunk = chunk => { // no more retries left if (chunk.retries.length === 0) return false; // new retry chunk.status = ChunkStatus.WAITING; clearTimeout(chunk.timeout); chunk.timeout = setTimeout(() => { processChunk(chunk); }, chunk.retries.shift()); // we're going to retry return true; }; const updateTotalProgress = () => { // calculate total progress fraction const totalBytesTransfered = chunks.reduce((p, chunk) => { if (p === null || chunk.progress === null) return null; return p + chunk.progress; }, 0); // can't compute progress if (totalBytesTransfered === null) return progress(false, 0, 0); // calculate progress values const totalSize = chunks.reduce((total, chunk) => total + chunk.size, 0); // can update progress indicator progress(true, totalBytesTransfered, totalSize); }; // process new chunks const processChunks = () => { const totalProcessing = chunks.filter(chunk => chunk.status === ChunkStatus.PROCESSING) .length; if (totalProcessing >= 1) return; processChunk(); }; const abortChunks = () => { chunks.forEach(chunk => { clearTimeout(chunk.timeout); if (chunk.request) { chunk.request.abort(); } }); }; // let's go! if (!state.serverId) { requestTransferId(serverId => { // stop here if aborted, might have happened in between request and callback if (state.aborted) return; // pass back to item so we can use it if something goes wrong transfer(serverId); // store internally state.serverId = serverId; processChunks(); }); } else { requestTransferOffset(offset => { // stop here if aborted, might have happened in between request and callback if (state.aborted) return; // mark chunks with lower offset as complete chunks .filter(chunk => chunk.offset < offset) .forEach(chunk => { chunk.status = ChunkStatus.COMPLETE; chunk.progress = chunk.size; }); // continue processing processChunks(); }); } return { abort: () => { state.aborted = true; abortChunks(); }, }; }; /* function signature: (file, metadata, load, error, progress, abort) => { return { abort:() => {} } } */ const createFileProcessorFunction = (apiUrl, action, name, options) => ( file, metadata, load, error, progress, abort, transfer ) => { // no file received if (!file) return; // if was passed a file, and we can chunk it, exit here const canChunkUpload = options.chunkUploads; const shouldChunkUpload = canChunkUpload && file.size > options.chunkSize; const willChunkUpload = canChunkUpload && (shouldChunkUpload || options.chunkForce); if (file instanceof Blob && willChunkUpload) return processFileChunked( apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options ); // set handlers const ondata = action.ondata || (fd => fd); const onload = action.onload || (res => res); const onerror = action.onerror || (res => null); const headers = typeof action.headers === 'function' ? action.headers(file, metadata) || {} : { ...action.headers, }; const requestParams = { ...action, headers, }; // create formdata object var formData = new FormData(); // add metadata under same name if (isObject(metadata)) { formData.append(name, JSON.stringify(metadata)); } // Turn into an array of objects so no matter what the input, we can handle it the same way (file instanceof Blob ? [{ name: null, file }] : file).forEach(item => { formData.append( name, item.file, item.name === null ? item.file.name : `${item.name}${item.file.name}` ); }); // send request object const request = sendRequest(ondata(formData), buildURL(apiUrl, action.url), requestParams); request.onload = xhr => { load(createResponse('load', xhr.status, onload(xhr.response), xhr.getAllResponseHeaders())); }; request.onerror = xhr => { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.ontimeout = createTimeoutResponse(error); request.onprogress = progress; request.onabort = abort; // should return request return request; }; const createProcessorFunction = (apiUrl = '', action, name, options) => { // custom handler (should also handle file, load, error, progress and abort) if (typeof action === 'function') return (...params) => action(name, ...params, options); // no action supplied if (!action || !isString(action.url)) return null; // internal handler return createFileProcessorFunction(apiUrl, action, name, options); }; /* function signature: (uniqueFileId, load, error) => { } */ const createRevertFunction = (apiUrl = '', action) => { // is custom implementation if (typeof action === 'function') { return action; } // no action supplied, return stub function, interface will work, but file won't be removed if (!action || !isString(action.url)) { return (uniqueFileId, load) => load(); } // set onload hanlder const onload = action.onload || (res => res); const onerror = action.onerror || (res => null); // internal implementation return (uniqueFileId, load, error) => { const request = sendRequest( uniqueFileId, apiUrl + action.url, action // contains method, headers and withCredentials properties ); request.onload = xhr => { load( createResponse( 'load', xhr.status, onload(xhr.response), xhr.getAllResponseHeaders() ) ); }; request.onerror = xhr => { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.ontimeout = createTimeoutResponse(error); return request; }; }; const getRandomNumber = (min = 0, max = 1) => min + Math.random() * (max - min); const createPerceivedPerformanceUpdater = ( cb, duration = 1000, offset = 0, tickMin = 25, tickMax = 250 ) => { let timeout = null; const start = Date.now(); const tick = () => { let runtime = Date.now() - start; let delay = getRandomNumber(tickMin, tickMax); if (runtime + delay > duration) { delay = runtime + delay - duration; } let progress = runtime / duration; if (progress >= 1 || document.hidden) { cb(1); return; } cb(progress); timeout = setTimeout(tick, delay); }; if (duration > 0) tick(); return { clear: () => { clearTimeout(timeout); }, }; }; const createFileProcessor = (processFn, options) => { const state = { complete: false, perceivedProgress: 0, perceivedPerformanceUpdater: null, progress: null, timestamp: null, perceivedDuration: 0, duration: 0, request: null, response: null, }; const { allowMinimumUploadDuration } = options; const process = (file, metadata) => { const progressFn = () => { // we've not yet started the real download, stop here // the request might not go through, for instance, there might be some server trouble // if state.progress is null, the server does not allow computing progress and we show the spinner instead if (state.duration === 0 || state.progress === null) return; // as we're now processing, fire the progress event api.fire('progress', api.getProgress()); }; const completeFn = () => { state.complete = true; api.fire('load-perceived', state.response.body); }; // let's start processing api.fire('start'); // set request start state.timestamp = Date.now(); // create perceived performance progress indicator state.perceivedPerformanceUpdater = createPerceivedPerformanceUpdater( progress => { state.perceivedProgress = progress; state.perceivedDuration = Date.now() - state.timestamp; progressFn(); // if fake progress is done, and a response has been received, // and we've not yet called the complete method if (state.response && state.perceivedProgress === 1 && !state.complete) { // we done! completeFn(); } }, // random delay as in a list of files you start noticing // files uploading at the exact same speed allowMinimumUploadDuration ? getRandomNumber(750, 1500) : 0 ); // remember request so we can abort it later state.request = processFn( // the file to process file, // the metadata to send along metadata, // callbacks (load, error, progress, abort, transfer) // load expects the body to be a server id if // you want to make use of revert response => { // we put the response in state so we can access // it outside of this method state.response = isObject(response) ? response : { type: 'load', code: 200, body: `${response}`, headers: {}, }; // update duration state.duration = Date.now() - state.timestamp; // force progress to 1 as we're now done state.progress = 1; // actual load is done let's share results api.fire('load', state.response.body); // we are really done // if perceived progress is 1 ( wait for perceived progress to complete ) // or if server does not support progress ( null ) if ( !allowMinimumUploadDuration || (allowMinimumUploadDuration && state.perceivedProgress === 1) ) { completeFn(); } }, // error is expected to be an object with type, code, body error => { // cancel updater state.perceivedPerformanceUpdater.clear(); // update others about this error api.fire( 'error', isObject(error) ? error : { type: 'error', code: 0, body: `${error}`, } ); }, // actual processing progress (computable, current, total) => { // update actual duration state.duration = Date.now() - state.timestamp; // update actual progress state.progress = computable ? current / total : null; progressFn(); }, // abort does not expect a value () => { // stop updater state.perceivedPerformanceUpdater.clear(); // fire the abort event so we can switch visuals api.fire('abort', state.response ? state.response.body : null); }, // register the id for this transfer transferId => { api.fire('transfer', transferId); } ); }; const abort = () => { // no request running, can't abort if (!state.request) return; // stop updater state.perceivedPerformanceUpdater.clear(); // abort actual request if (state.request.abort) state.request.abort(); // if has response object, we've completed the request state.complete = true; }; const reset = () => { abort(); state.complete = false; state.perceivedProgress = 0; state.progress = 0; state.timestamp = null; state.perceivedDuration = 0; state.duration = 0; state.request = null; state.response = null; }; const getProgress = allowMinimumUploadDuration ? () => (state.progress ? Math.min(state.progress, state.perceivedProgress) : null) : () => state.progress || null; const getDuration = allowMinimumUploadDuration ? () => Math.min(state.duration, state.perceivedDuration) : () => state.duration; const api = { ...on(), process, // start processing file abort, // abort active process request getProgress, getDuration, reset, }; return api; }; const getFilenameWithoutExtension = name => name.substring(0, name.lastIndexOf('.')) || name; const createFileStub = source => { let data = [source.name, source.size, source.type]; // is blob or base64, then we need to set the name if (source instanceof Blob || isBase64DataURI(source)) { data[0] = source.name || getDateString(); } else if (isBase64DataURI(source)) { // if is base64 data uri we need to determine the average size and type data[1] = source.length; data[2] = getMimeTypeFromBase64DataURI(source); } else if (isString(source)) { // url data[0] = getFilenameFromURL(source); data[1] = 0; data[2] = 'application/octet-stream'; } return { name: data[0], size: data[1], type: data[2], }; }; const isFile = value => !!(value instanceof File || (value instanceof Blob && value.name)); const deepCloneObject = src => { if (!isObject(src)) return src; const target = isArray(src) ? [] : {}; for (const key in src) { if (!src.hasOwnProperty(key)) continue; const v = src[key]; target[key] = v && isObject(v) ? deepCloneObject(v) : v; } return target; }; const createItem = (origin = null, serverFileReference = null, file = null) => { // unique id for this item, is used to identify the item across views const id = getUniqueId(); /** * Internal item state */ const state = { // is archived archived: false, // if is frozen, no longer fires events frozen: false, // removed from view released: false, // original source source: null, // file model reference file, // id of file on server serverFileReference, // id of file transfer on server transferId: null, // is aborted processingAborted: false, // current item status status: serverFileReference ? ItemStatus.PROCESSING_COMPLETE : ItemStatus.INIT, // active processes activeLoader: null, activeProcessor: null, }; // callback used when abort processing is called to link back to the resolve method let abortProcessingRequestComplete = null; /** * Externally added item metadata */ const metadata = {}; // item data const setStatus = status => (state.status = status); // fire event unless the item has been archived const fire = (event, ...params) => { if (state.released || state.frozen) return; api.fire(event, ...params); }; // file data const getFileExtension = () => getExtensionFromFilename(state.file.name); const getFileType = () => state.file.type; const getFileSize = () => state.file.size; const getFile = () => state.file; // // logic to load a file // const load = (source, loader, onload) => { // remember the original item source state.source = source; // source is known api.fireSync('init'); // file stub is already there if (state.file) { api.fireSync('load-skip'); return; } // set a stub file object while loading the actual data state.file = createFileStub(source); // starts loading loader.on('init', () => { fire('load-init'); }); // we'eve received a size indication, let's update the stub loader.on('meta', meta => { // set size of file stub state.file.size = meta.size; // set name of file stub state.file.filename = meta.filename; // if has received source, we done if (meta.source) { origin = FileOrigin.LIMBO; state.serverFileReference = meta.source; state.status = ItemStatus.PROCESSING_COMPLETE; } // size has been updated fire('load-meta'); }); // the file is now loading we need to update the progress indicators loader.on('progress', progress => { setStatus(ItemStatus.LOADING); fire('load-progress', progress); }); // an error was thrown while loading the file, we need to switch to error state loader.on('error', error => { setStatus(ItemStatus.LOAD_ERROR); fire('load-request-error', error); }); // user or another process aborted the file load (cannot retry) loader.on('abort', () => { setStatus(ItemStatus.INIT); fire('load-abort'); }); // done loading loader.on('load', file => { // as we've now loaded the file the loader is no longer required state.activeLoader = null; // called when file has loaded succesfully const success = result => { // set (possibly) transformed file state.file = isFile(result) ? result : state.file; // file received if (origin === FileOrigin.LIMBO && state.serverFileReference) { setStatus(ItemStatus.PROCESSING_COMPLETE); } else { setStatus(ItemStatus.IDLE); } fire('load'); }; const error = result => { // set original file state.file = file; fire('load-meta'); setStatus(ItemStatus.LOAD_ERROR); fire('load-file-error', result); }; // if we already have a server file reference, we don't need to call the onload method if (state.serverFileReference) { success(file); return; } // no server id, let's give this file the full treatment onload(file, success, error); }); // set loader source data loader.setSource(source); // set as active loader state.activeLoader = loader; // load the source data loader.load(); }; const retryLoad = () => { if (!state.activeLoader) { return; } state.activeLoader.load(); }; const abortLoad = () => { if (state.activeLoader) { state.activeLoader.abort(); return; } setStatus(ItemStatus.INIT); fire('load-abort'); }; // // logic to process a file // const process = (processor, onprocess) => { // processing was aborted if (state.processingAborted) { state.processingAborted = false; return; } // now processing setStatus(ItemStatus.PROCESSING); // reset abort callback abortProcessingRequestComplete = null; // if no file loaded we'll wait for the load event if (!(state.file instanceof Blob)) { api.on('load', () => { process(processor, onprocess); }); return; } // setup processor processor.on('load', serverFileReference => { // need this id to be able to revert the upload state.transferId = null; state.serverFileReference = serverFileReference; }); // register transfer id processor.on('transfer', transferId => { // need this id to be able to revert the upload state.transferId = transferId; }); processor.on('load-perceived', serverFileReference => { // no longer required state.activeProcessor = null; // need this id to be able to rever the upload state.transferId = null; state.serverFileReference = serverFileReference; setStatus(ItemStatus.PROCESSING_COMPLETE); fire('process-complete', serverFileReference); }); processor.on('start', () => { fire('process-start'); }); processor.on('error', error => { state.activeProcessor = null; setStatus(ItemStatus.PROCESSING_ERROR); fire('process-error', error); }); processor.on('abort', serverFileReference => { state.activeProcessor = null; // if file was uploaded but processing was cancelled during perceived processor time store file reference state.serverFileReference = serverFileReference; setStatus(ItemStatus.IDLE); fire('process-abort'); // has timeout so doesn't interfere with remove action if (abortProcessingRequestComplete) { abortProcessingRequestComplete(); } }); processor.on('progress', progress => { fire('process-progress', progress); }); // when successfully transformed const success = file => { // if was archived in the mean time, don't process if (state.archived) return; // process file! processor.process(file, { ...metadata }); }; // something went wrong during transform phase const error = console.error; // start processing the file onprocess(state.file, success, error); // set as active processor state.activeProcessor = processor; }; const requestProcessing = () => { state.processingAborted = false; setStatus(ItemStatus.PROCESSING_QUEUED); }; const abortProcessing = () => new Promise(resolve => { if (!state.activeProcessor) { state.processingAborted = true; setStatus(ItemStatus.IDLE); fire('process-abort'); resolve(); return; } abortProcessingRequestComplete = () => { resolve(); }; state.activeProcessor.abort(); }); // // logic to revert a processed file // const revert = (revertFileUpload, forceRevert) => new Promise((resolve, reject) => { // a completed upload will have a serverFileReference, a failed chunked upload where // getting a serverId succeeded but >=0 chunks have been uploaded will have transferId set const serverTransferId = state.serverFileReference !== null ? state.serverFileReference : state.transferId; // cannot revert without a server id for this process if (serverTransferId === null) { resolve(); return; } // revert the upload (fire and forget) revertFileUpload( serverTransferId, () => { // reset file server id and transfer id as now it's not available on the server state.serverFileReference = null; state.transferId = null; resolve(); }, error => { // don't set error state when reverting is optional, it will always resolve if (!forceRevert) { resolve(); return; } // oh no errors setStatus(ItemStatus.PROCESSING_REVERT_ERROR); fire('process-revert-error'); reject(error); } ); // fire event setStatus(ItemStatus.IDLE); fire('process-revert'); }); // exposed methods const setMetadata = (key, value, silent) => { const keys = key.split('.'); const root = keys[0]; const last = keys.pop(); let data = metadata; keys.forEach(key => (data = data[key])); // compare old value against new value, if they're the same, we're not updating if (JSON.stringify(data[last]) === JSON.stringify(value)) return; // update value data[last] = value; // fire update fire('metadata-update', { key: root, value: metadata[root], silent, }); }; const getMetadata = key => deepCloneObject(key ? metadata[key] : metadata); const api = { id: { get: () => id }, origin: { get: () => origin, set: value => (origin = value) }, serverId: { get: () => state.serverFileReference }, transferId: { get: () => state.transferId }, status: { get: () => state.status }, filename: { get: () => state.file.name }, filenameWithoutExtension: { get: () => getFilenameWithoutExtension(state.file.name) }, fileExtension: { get: getFileExtension }, fileType: { get: getFileType }, fileSize: { get: getFileSize }, file: { get: getFile }, relativePath: { get: () => state.file._relativePath }, source: { get: () => state.source }, getMetadata, setMetadata: (key, value, silent) => { if (isObject(key)) { const data = key; Object.keys(data).forEach(key => { setMetadata(key, data[key], value); }); return key; } setMetadata(key, value, silent); return value; }, extend: (name, handler) => (itemAPI[name] = handler), abortLoad, retryLoad, requestProcessing, abortProcessing, load, process, revert, ...on(), freeze: () => (state.frozen = true), release: () => (state.released = true), released: { get: () => state.released }, archive: () => (state.archived = true), archived: { get: () => state.archived }, // replace source and file object setFile: file => (state.file = file), }; // create it here instead of returning it instantly so we can extend it later const itemAPI = createObject(api); return itemAPI; }; const getItemIndexByQuery = (items, query) => { // just return first index if (isEmpty(query)) { return 0; } // invalid queries if (!isString(query)) { return -1; } // return item by id (or -1 if not found) return items.findIndex(item => item.id === query); }; const getItemById = (items, itemId) => { const index = getItemIndexByQuery(items, itemId); if (index < 0) { return; } return items[index] || null; }; const fetchBlob = (url, load, error, progress, abort, headers) => { const request = sendRequest(null, url, { method: 'GET', responseType: 'blob', }); request.onload = xhr => { // get headers const headers = xhr.getAllResponseHeaders(); // get filename const filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url); // create response load(createResponse('load', xhr.status, getFileFromBlob(xhr.response, filename), headers)); }; request.onerror = xhr => { error(createResponse('error', xhr.status, xhr.statusText, xhr.getAllResponseHeaders())); }; request.onheaders = xhr => { headers(createResponse('headers', xhr.status, null, xhr.getAllResponseHeaders())); }; request.ontimeout = createTimeoutResponse(error); request.onprogress = progress; request.onabort = abort; // should return request return request; }; const getDomainFromURL = url => { if (url.indexOf('//') === 0) { url = location.protocol + url; } return url .toLowerCase() .replace('blob:', '') .replace(/([a-z])?:\/\//, '$1') .split('/')[0]; }; const isExternalURL = url => (url.indexOf(':') > -1 || url.indexOf('//') > -1) && getDomainFromURL(location.href) !== getDomainFromURL(url); const dynamicLabel = label => (...params) => (isFunction(label) ? label(...params) : label); const isMockItem = item => !isFile(item.file); const listUpdated = (dispatch, state) => { clearTimeout(state.listUpdateTimeout); state.listUpdateTimeout = setTimeout(() => { dispatch('DID_UPDATE_ITEMS', { items: getActiveItems(state.items) }); }, 0); }; const optionalPromise = (fn, ...params) => new Promise(resolve => { if (!fn) { return resolve(true); } const result = fn(...params); if (result == null) { return resolve(true); } if (typeof result === 'boolean') { return resolve(result); } if (typeof result.then === 'function') { result.then(resolve); } }); const sortItems = (state, compare) => { state.items.sort((a, b) => compare(createItemAPI(a), createItemAPI(b))); }; // returns item based on state const getItemByQueryFromState = (state, itemHandler) => ({ query, success = () => {}, failure = () => {}, ...options } = {}) => { const item = getItemByQuery(state.items, query); if (!item) { failure({ error: createResponse('error', 0, 'Item not found'), file: null, }); return; } itemHandler(item, success, failure, options || {}); }; const actions = (dispatch, query, state) => ({ /** * Aborts all ongoing processes */ ABORT_ALL: () => { getActiveItems(state.items).forEach(item => { item.freeze(); item.abortLoad(); item.abortProcessing(); }); }, /** * Sets initial files */ DID_SET_FILES: ({ value = [] }) => { // map values to file objects const files = value.map(file => ({ source: file.source ? file.source : file, options: file.options, })); // loop over files, if file is in list, leave it be, if not, remove // test if items should be moved let activeItems = getActiveItems(state.items); activeItems.forEach(item => { // if item not is in new value, remove if (!files.find(file => file.source === item.source || file.source === item.file)) { dispatch('REMOVE_ITEM', { query: item, remove: false }); } }); // add new files activeItems = getActiveItems(state.items); files.forEach((file, index) => { // if file is already in list if (activeItems.find(item => item.source === file.source || item.file === file.source)) return; // not in list, add dispatch('ADD_ITEM', { ...file, interactionMethod: InteractionMethod.NONE, index, }); }); }, DID_UPDATE_ITEM_METADATA: ({ id, action, change }) => { // don't do anything if (change.silent) return; // if is called multiple times in close succession we combined all calls together to save resources clearTimeout(state.itemUpdateTimeout); state.itemUpdateTimeout = setTimeout(() => { const item = getItemById(state.items, id); // only revert and attempt to upload when we're uploading to a server if (!query('IS_ASYNC')) { // should we update the output data applyFilterChain('SHOULD_PREPARE_OUTPUT', false, { item, query, action, change, }).then(shouldPrepareOutput => { // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook const beforePrepareFile = query('GET_BEFORE_PREPARE_FILE'); if (beforePrepareFile) shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput); if (!shouldPrepareOutput) return; dispatch( 'REQUEST_PREPARE_OUTPUT', { query: id, item, success: file => { dispatch('DID_PREPARE_OUTPUT', { id, file }); }, }, true ); }); return; } // if is local item we need to enable upload button so change can be propagated to server if (item.origin === FileOrigin.LOCAL) { dispatch('DID_LOAD_ITEM', { id: item.id, error: null, serverFileReference: item.source, }); } // for async scenarios const upload = () => { // we push this forward a bit so the interface is updated correctly setTimeout(() => { dispatch('REQUEST_ITEM_PROCESSING', { query: id }); }, 32); }; const revert = doUpload => { item.revert( createRevertFunction(state.options.server.url, state.options.server.revert), query('GET_FORCE_REVERT') ) .then(doUpload ? upload : () => {}) .catch(() => {}); }; const abort = doUpload => { item.abortProcessing().then(doUpload ? upload : () => {}); }; // if we should re-upload the file immediately if (item.status === ItemStatus.PROCESSING_COMPLETE) { return revert(state.options.instantUpload); } // if currently uploading, cancel upload if (item.status === ItemStatus.PROCESSING) { return abort(state.options.instantUpload); } if (state.options.instantUpload) { upload(); } }, 0); }, MOVE_ITEM: ({ query, index }) => { const item = getItemByQuery(state.items, query); if (!item) return; const currentIndex = state.items.indexOf(item); index = limit(index, 0, state.items.length - 1); if (currentIndex === index) return; state.items.splice(index, 0, state.items.splice(currentIndex, 1)[0]); }, SORT: ({ compare }) => { sortItems(state, compare); dispatch('DID_SORT_ITEMS', { items: query('GET_ACTIVE_ITEMS'), }); }, ADD_ITEMS: ({ items, index, interactionMethod, success = () => {}, failure = () => {} }) => { let currentIndex = index; if (index === -1 || typeof index === 'undefined') { const insertLocation = query('GET_ITEM_INSERT_LOCATION'); const totalItems = query('GET_TOTAL_ITEMS'); currentIndex = insertLocation === 'before' ? 0 : totalItems; } const ignoredFiles = query('GET_IGNORED_FILES'); const isValidFile = source => isFile(source) ? !ignoredFiles.includes(source.name.toLowerCase()) : !isEmpty(source); const validItems = items.filter(isValidFile); const promises = validItems.map( source => new Promise((resolve, reject) => { dispatch('ADD_ITEM', { interactionMethod, source: source.source || source, success: resolve, failure: reject, index: currentIndex++, options: source.options || {}, }); }) ); Promise.all(promises) .then(success) .catch(failure); }, /** * @param source * @param index * @param interactionMethod */ ADD_ITEM: ({ source, index = -1, interactionMethod, success = () => {}, failure = () => {}, options = {}, }) => { // if no source supplied if (isEmpty(source)) { failure({ error: createResponse('error', 0, 'No source'), file: null, }); return; } // filter out invalid file items, used to filter dropped directory contents if (isFile(source) && state.options.ignoredFiles.includes(source.name.toLowerCase())) { // fail silently return; } // test if there's still room in the list of files if (!hasRoomForItem(state)) { // if multiple allowed, we can't replace // or if only a single item is allowed but we're not allowed to replace it we exit if ( state.options.allowMultiple || (!state.options.allowMultiple && !state.options.allowReplace) ) { const error = createResponse('warning', 0, 'Max files'); dispatch('DID_THROW_MAX_FILES', { source, error, }); failure({ error, file: null }); return; } // let's replace the item // id of first item we're about to remove const item = getActiveItems(state.items)[0]; // if has been processed remove it from the server as well if ( item.status === ItemStatus.PROCESSING_COMPLETE || item.status === ItemStatus.PROCESSING_REVERT_ERROR ) { const forceRevert = query('GET_FORCE_REVERT'); item.revert( createRevertFunction(state.options.server.url, state.options.server.revert), forceRevert ) .then(() => { if (!forceRevert) return; // try to add now dispatch('ADD_ITEM', { source, index, interactionMethod, success, failure, options, }); }) .catch(() => {}); // no need to handle this catch state for now if (forceRevert) return; } // remove first item as it will be replaced by this item dispatch('REMOVE_ITEM', { query: item.id }); } // where did the file originate const origin = options.type === 'local' ? FileOrigin.LOCAL : options.type === 'limbo' ? FileOrigin.LIMBO : FileOrigin.INPUT; // create a new blank item const item = createItem( // where did this file come from origin, // an input file never has a server file reference origin === FileOrigin.INPUT ? null : source, // file mock data, if defined options.file ); // set initial meta data Object.keys(options.metadata || {}).forEach(key => { item.setMetadata(key, options.metadata[key]); }); // created the item, let plugins add methods applyFilters('DID_CREATE_ITEM', item, { query, dispatch }); // where to insert new items const itemInsertLocation = query('GET_ITEM_INSERT_LOCATION'); // adjust index if is not allowed to pick location if (!state.options.itemInsertLocationFreedom) { index = itemInsertLocation === 'before' ? -1 : state.items.length; } // add item to list insertItem(state.items, item, index); // sort items in list if (isFunction(itemInsertLocation) && source) { sortItems(state, itemInsertLocation); } // get a quick reference to the item id const id = item.id; // observe item events item.on('init', () => { dispatch('DID_INIT_ITEM', { id }); }); item.on('load-init', () => { dispatch('DID_START_ITEM_LOAD', { id }); }); item.on('load-meta', () => { dispatch('DID_UPDATE_ITEM_META', { id }); }); item.on('load-progress', progress => { dispatch('DID_UPDATE_ITEM_LOAD_PROGRESS', { id, progress }); }); item.on('load-request-error', error => { const mainStatus = dynamicLabel(state.options.labelFileLoadError)(error); // is client error, no way to recover if (error.code >= 400 && error.code < 500) { dispatch('DID_THROW_ITEM_INVALID', { id, error, status: { main: mainStatus, sub: `${error.code} (${error.body})`, }, }); // reject the file so can be dealt with through API failure({ error, file: createItemAPI(item) }); return; } // is possible server error, so might be possible to retry dispatch('DID_THROW_ITEM_LOAD_ERROR', { id, error, status: { main: mainStatus, sub: state.options.labelTapToRetry, }, }); }); item.on('load-file-error', error => { dispatch('DID_THROW_ITEM_INVALID', { id, error: error.status, status: error.status, }); failure({ error: error.status, file: createItemAPI(item) }); }); item.on('load-abort', () => { dispatch('REMOVE_ITEM', { query: id }); }); item.on('load-skip', () => { item.on('metadata-update', change => { if (!isFile(item.file)) return; dispatch('DID_UPDATE_ITEM_METADATA', { id, change }); }); dispatch('COMPLETE_LOAD_ITEM', { query: id, item, data: { source, success, }, }); }); item.on('load', () => { const handleAdd = shouldAdd => { // no should not add this file if (!shouldAdd) { dispatch('REMOVE_ITEM', { query: id, }); return; } // now interested in metadata updates item.on('metadata-update', change => { dispatch('DID_UPDATE_ITEM_METADATA', { id, change }); }); // let plugins decide if the output data should be prepared at this point // means we'll do this and wait for idle state applyFilterChain('SHOULD_PREPARE_OUTPUT', false, { item, query }).then( shouldPrepareOutput => { // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook const beforePrepareFile = query('GET_BEFORE_PREPARE_FILE'); if (beforePrepareFile) shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput); const loadComplete = () => { dispatch('COMPLETE_LOAD_ITEM', { query: id, item, data: { source, success, }, }); listUpdated(dispatch, state); }; // exit if (shouldPrepareOutput) { // wait for idle state and then run PREPARE_OUTPUT dispatch( 'REQUEST_PREPARE_OUTPUT', { query: id, item, success: file => { dispatch('DID_PREPARE_OUTPUT', { id, file }); loadComplete(); }, }, true ); return; } loadComplete(); } ); }; // item loaded, allow plugins to // - read data (quickly) // - add metadata applyFilterChain('DID_LOAD_ITEM', item, { query, dispatch }) .then(() => { optionalPromise(query('GET_BEFORE_ADD_FILE'), createItemAPI(item)).then( handleAdd ); }) .catch(e => { if (!e || !e.error || !e.status) return handleAdd(false); dispatch('DID_THROW_ITEM_INVALID', { id, error: e.error, status: e.status, }); }); }); item.on('process-start', () => { dispatch('DID_START_ITEM_PROCESSING', { id }); }); item.on('process-progress', progress => { dispatch('DID_UPDATE_ITEM_PROCESS_PROGRESS', { id, progress }); }); item.on('process-error', error => { dispatch('DID_THROW_ITEM_PROCESSING_ERROR', { id, error, status: { main: dynamicLabel(state.options.labelFileProcessingError)(error), sub: state.options.labelTapToRetry, }, }); }); item.on('process-revert-error', error => { dispatch('DID_THROW_ITEM_PROCESSING_REVERT_ERROR', { id, error, status: { main: dynamicLabel(state.options.labelFileProcessingRevertError)(error), sub: state.options.labelTapToRetry, }, }); }); item.on('process-complete', serverFileReference => { dispatch('DID_COMPLETE_ITEM_PROCESSING', { id, error: null, serverFileReference, }); dispatch('DID_DEFINE_VALUE', { id, value: serverFileReference }); }); item.on('process-abort', () => { dispatch('DID_ABORT_ITEM_PROCESSING', { id }); }); item.on('process-revert', () => { dispatch('DID_REVERT_ITEM_PROCESSING', { id }); dispatch('DID_DEFINE_VALUE', { id, value: null }); }); // let view know the item has been inserted dispatch('DID_ADD_ITEM', { id, index, interactionMethod }); listUpdated(dispatch, state); // start loading the source const { url, load, restore, fetch } = state.options.server || {}; item.load( source, // this creates a function that loads the file based on the type of file (string, base64, blob, file) and location of file (local, remote, limbo) createFileLoader( origin === FileOrigin.INPUT ? // input, if is remote, see if should use custom fetch, else use default fetchBlob isString(source) && isExternalURL(source) ? fetch ? createFetchFunction(url, fetch) : fetchBlob // remote url : fetchBlob // try to fetch url : // limbo or local origin === FileOrigin.LIMBO ? createFetchFunction(url, restore) // limbo : createFetchFunction(url, load) // local ), // called when the file is loaded so it can be piped through the filters (file, success, error) => { // let's process the file applyFilterChain('LOAD_FILE', file, { query }) .then(success) .catch(error); } ); }, REQUEST_PREPARE_OUTPUT: ({ item, success, failure = () => {} }) => { // error response if item archived const err = { error: createResponse('error', 0, 'Item not found'), file: null, }; // don't handle archived items, an item could have been archived (load aborted) while waiting to be prepared if (item.archived) return failure(err); // allow plugins to alter the file data applyFilterChain('PREPARE_OUTPUT', item.file, { query, item }).then(result => { applyFilterChain('COMPLETE_PREPARE_OUTPUT', result, { query, item }).then(result => { // don't handle archived items, an item could have been archived (load aborted) while being prepared if (item.archived) return failure(err); // we done! success(result); }); }); }, COMPLETE_LOAD_ITEM: ({ item, data }) => { const { success, source } = data; // sort items in list const itemInsertLocation = query('GET_ITEM_INSERT_LOCATION'); if (isFunction(itemInsertLocation) && source) { sortItems(state, itemInsertLocation); } // let interface know the item has loaded dispatch('DID_LOAD_ITEM', { id: item.id, error: null, serverFileReference: item.origin === FileOrigin.INPUT ? null : source, }); // item has been successfully loaded and added to the // list of items so can now be safely returned for use success(createItemAPI(item)); // if this is a local server file we need to show a different state if (item.origin === FileOrigin.LOCAL) { dispatch('DID_LOAD_LOCAL_ITEM', { id: item.id }); return; } // if is a temp server file we prevent async upload call here (as the file is already on the server) if (item.origin === FileOrigin.LIMBO) { dispatch('DID_COMPLETE_ITEM_PROCESSING', { id: item.id, error: null, serverFileReference: source, }); dispatch('DID_DEFINE_VALUE', { id: item.id, value: item.serverId || source, }); return; } // id we are allowed to upload the file immediately, lets do it if (query('IS_ASYNC') && state.options.instantUpload) { dispatch('REQUEST_ITEM_PROCESSING', { query: item.id }); } }, RETRY_ITEM_LOAD: getItemByQueryFromState(state, item => { // try loading the source one more time item.retryLoad(); }), REQUEST_ITEM_PREPARE: getItemByQueryFromState(state, (item, success, failure) => { dispatch( 'REQUEST_PREPARE_OUTPUT', { query: item.id, item, success: file => { dispatch('DID_PREPARE_OUTPUT', { id: item.id, file }); success({ file: item, output: file, }); }, failure, }, true ); }), REQUEST_ITEM_PROCESSING: getItemByQueryFromState(state, (item, success, failure) => { // cannot be queued (or is already queued) const itemCanBeQueuedForProcessing = // waiting for something item.status === ItemStatus.IDLE || // processing went wrong earlier item.status === ItemStatus.PROCESSING_ERROR; // not ready to be processed if (!itemCanBeQueuedForProcessing) { const processNow = () => dispatch('REQUEST_ITEM_PROCESSING', { query: item, success, failure }); const process = () => (document.hidden ? processNow() : setTimeout(processNow, 32)); // if already done processing or tried to revert but didn't work, try again if ( item.status === ItemStatus.PROCESSING_COMPLETE || item.status === ItemStatus.PROCESSING_REVERT_ERROR ) { item.revert( createRevertFunction(state.options.server.url, state.options.server.revert), query('GET_FORCE_REVERT') ) .then(process) .catch(() => {}); // don't continue with processing if something went wrong } else if (item.status === ItemStatus.PROCESSING) { item.abortProcessing().then(process); } return; } // already queued for processing if (item.status === ItemStatus.PROCESSING_QUEUED) return; item.requestProcessing(); dispatch('DID_REQUEST_ITEM_PROCESSING', { id: item.id }); dispatch('PROCESS_ITEM', { query: item, success, failure }, true); }), PROCESS_ITEM: getItemByQueryFromState(state, (item, success, failure) => { const maxParallelUploads = query('GET_MAX_PARALLEL_UPLOADS'); const totalCurrentUploads = query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING).length; // queue and wait till queue is freed up if (totalCurrentUploads === maxParallelUploads) { // queue for later processing state.processingQueue.push({ id: item.id, success, failure, }); // stop it! return; } // if was not queued or is already processing exit here if (item.status === ItemStatus.PROCESSING) return; const processNext = () => { // process queueud items const queueEntry = state.processingQueue.shift(); // no items left if (!queueEntry) return; // get item reference const { id, success, failure } = queueEntry; const itemReference = getItemByQuery(state.items, id); // if item was archived while in queue, jump to next if (!itemReference || itemReference.archived) { processNext(); return; } // process queued item dispatch('PROCESS_ITEM', { query: id, success, failure }, true); }; // we done function item.onOnce('process-complete', () => { success(createItemAPI(item)); processNext(); // if origin is local, and we're instant uploading, trigger remove of original // as revert will remove file from list const server = state.options.server; const instantUpload = state.options.instantUpload; if (instantUpload && item.origin === FileOrigin.LOCAL && isFunction(server.remove)) { const noop = () => {}; item.origin = FileOrigin.LIMBO; state.options.server.remove(item.source, noop, noop); } // All items processed? No errors? const allItemsProcessed = query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING_COMPLETE).length === state.items.length; if (allItemsProcessed) { dispatch('DID_COMPLETE_ITEM_PROCESSING_ALL'); } }); // we error function item.onOnce('process-error', error => { failure({ error, file: createItemAPI(item) }); processNext(); }); // abort function item.onOnce('process-abort', () => { processNext(); }); // start file processing const options = state.options; item.process( createFileProcessor( createProcessorFunction(options.server.url, options.server.process, options.name, { chunkTransferId: item.transferId, chunkServer: options.server.patch, chunkUploads: options.chunkUploads, chunkForce: options.chunkForce, chunkSize: options.chunkSize, chunkRetryDelays: options.chunkRetryDelays, }), { allowMinimumUploadDuration: query('GET_ALLOW_MINIMUM_UPLOAD_DURATION'), } ), // called when the file is about to be processed so it can be piped through the transform filters (file, success, error) => { // allow plugins to alter the file data applyFilterChain('PREPARE_OUTPUT', file, { query, item }) .then(file => { dispatch('DID_PREPARE_OUTPUT', { id: item.id, file }); success(file); }) .catch(error); } ); }), RETRY_ITEM_PROCESSING: getItemByQueryFromState(state, item => { dispatch('REQUEST_ITEM_PROCESSING', { query: item }); }), REQUEST_REMOVE_ITEM: getItemByQueryFromState(state, item => { optionalPromise(query('GET_BEFORE_REMOVE_FILE'), createItemAPI(item)).then(shouldRemove => { if (!shouldRemove) { return; } dispatch('REMOVE_ITEM', { query: item }); }); }), RELEASE_ITEM: getItemByQueryFromState(state, item => { item.release(); }), REMOVE_ITEM: getItemByQueryFromState(state, (item, success, failure, options) => { const removeFromView = () => { // get id reference const id = item.id; // archive the item, this does not remove it from the list getItemById(state.items, id).archive(); // tell the view the item has been removed dispatch('DID_REMOVE_ITEM', { error: null, id, item }); // now the list has been modified listUpdated(dispatch, state); // correctly removed success(createItemAPI(item)); }; // if this is a local file and the `server.remove` function has been configured, // send source there so dev can remove file from server const server = state.options.server; if ( item.origin === FileOrigin.LOCAL && server && isFunction(server.remove) && options.remove !== false ) { dispatch('DID_START_ITEM_REMOVE', { id: item.id }); server.remove( item.source, () => removeFromView(), status => { dispatch('DID_THROW_ITEM_REMOVE_ERROR', { id: item.id, error: createResponse('error', 0, status, null), status: { main: dynamicLabel(state.options.labelFileRemoveError)(status), sub: state.options.labelTapToRetry, }, }); } ); } else { // if is requesting revert and can revert need to call revert handler (not calling request_ because that would also trigger beforeRemoveHook) if ( (options.revert && item.origin !== FileOrigin.LOCAL && item.serverId !== null) || // if chunked uploads are enabled and we're uploading in chunks for this specific file // or if the file isn't big enough for chunked uploads but chunkForce is set then call // revert before removing from the view... (state.options.chunkUploads && item.file.size > state.options.chunkSize) || (state.options.chunkUploads && state.options.chunkForce) ) { item.revert( createRevertFunction(state.options.server.url, state.options.server.revert), query('GET_FORCE_REVERT') ); } // can now safely remove from view removeFromView(); } }), ABORT_ITEM_LOAD: getItemByQueryFromState(state, item => { item.abortLoad(); }), ABORT_ITEM_PROCESSING: getItemByQueryFromState(state, item => { // test if is already processed if (item.serverId) { dispatch('REVERT_ITEM_PROCESSING', { id: item.id }); return; } // abort item.abortProcessing().then(() => { const shouldRemove = state.options.instantUpload; if (shouldRemove) { dispatch('REMOVE_ITEM', { query: item.id }); } }); }), REQUEST_REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, item => { // not instant uploading, revert immediately if (!state.options.instantUpload) { dispatch('REVERT_ITEM_PROCESSING', { query: item }); return; } // if we're instant uploading the file will also be removed if we revert, // so if a before remove file hook is defined we need to run it now const handleRevert = shouldRevert => { if (!shouldRevert) return; dispatch('REVERT_ITEM_PROCESSING', { query: item }); }; const fn = query('GET_BEFORE_REMOVE_FILE'); if (!fn) { return handleRevert(true); } const requestRemoveResult = fn(createItemAPI(item)); if (requestRemoveResult == null) { // undefined or null return handleRevert(true); } if (typeof requestRemoveResult === 'boolean') { return handleRevert(requestRemoveResult); } if (typeof requestRemoveResult.then === 'function') { requestRemoveResult.then(handleRevert); } }), REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, item => { item.revert( createRevertFunction(state.options.server.url, state.options.server.revert), query('GET_FORCE_REVERT') ) .then(() => { const shouldRemove = state.options.instantUpload || isMockItem(item); if (shouldRemove) { dispatch('REMOVE_ITEM', { query: item.id }); } }) .catch(() => {}); }), SET_OPTIONS: ({ options }) => { // get all keys passed const optionKeys = Object.keys(options); // get prioritized keyed to include (remove once not in options object) const prioritizedOptionKeys = PrioritizedOptions.filter(key => optionKeys.includes(key)); // order the keys, prioritized first, then rest const orderedOptionKeys = [ // add prioritized first if passed to options, else remove ...prioritizedOptionKeys, // prevent duplicate keys ...Object.keys(options).filter(key => !prioritizedOptionKeys.includes(key)), ]; // dispatch set event for each option orderedOptionKeys.forEach(key => { dispatch(`SET_${fromCamels(key, '_').toUpperCase()}`, { value: options[key], }); }); }, }); const PrioritizedOptions = [ 'server', // must be processed before "files" ]; const formatFilename = name => name; const createElement$1 = tagName => { return document.createElement(tagName); }; const text = (node, value) => { let textNode = node.childNodes[0]; if (!textNode) { textNode = document.createTextNode(value); node.appendChild(textNode); } else if (value !== textNode.nodeValue) { textNode.nodeValue = value; } }; const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => { const angleInRadians = (((angleInDegrees % 360) - 90) * Math.PI) / 180.0; return { x: centerX + radius * Math.cos(angleInRadians), y: centerY + radius * Math.sin(angleInRadians), }; }; const describeArc = (x, y, radius, startAngle, endAngle, arcSweep) => { const start = polarToCartesian(x, y, radius, endAngle); const end = polarToCartesian(x, y, radius, startAngle); return ['M', start.x, start.y, 'A', radius, radius, 0, arcSweep, 0, end.x, end.y].join(' '); }; const percentageArc = (x, y, radius, from, to) => { let arcSweep = 1; if (to > from && to - from <= 0.5) { arcSweep = 0; } if (from > to && from - to >= 0.5) { arcSweep = 0; } return describeArc( x, y, radius, Math.min(0.9999, from) * 360, Math.min(0.9999, to) * 360, arcSweep ); }; const create = ({ root, props }) => { // start at 0 props.spin = false; props.progress = 0; props.opacity = 0; // svg const svg = createElement('svg'); root.ref.path = createElement('path', { 'stroke-width': 2, 'stroke-linecap': 'round', }); svg.appendChild(root.ref.path); root.ref.svg = svg; root.appendChild(svg); }; const write = ({ root, props }) => { if (props.opacity === 0) { return; } if (props.align) { root.element.dataset.align = props.align; } // get width of stroke const ringStrokeWidth = parseInt(attr(root.ref.path, 'stroke-width'), 10); // calculate size of ring const size = root.rect.element.width * 0.5; // ring state let ringFrom = 0; let ringTo = 0; // now in busy mode if (props.spin) { ringFrom = 0; ringTo = 0.5; } else { ringFrom = 0; ringTo = props.progress; } // get arc path const coordinates = percentageArc(size, size, size - ringStrokeWidth, ringFrom, ringTo); // update progress bar attr(root.ref.path, 'd', coordinates); // hide while contains 0 value attr(root.ref.path, 'stroke-opacity', props.spin || props.progress > 0 ? 1 : 0); }; const progressIndicator = createView({ tag: 'div', name: 'progress-indicator', ignoreRectUpdate: true, ignoreRect: true, create, write, mixins: { apis: ['progress', 'spin', 'align'], styles: ['opacity'], animations: { opacity: { type: 'tween', duration: 500 }, progress: { type: 'spring', stiffness: 0.95, damping: 0.65, mass: 10, }, }, }, }); const create$1 = ({ root, props }) => { root.element.innerHTML = (props.icon || '') + `${props.label}`; props.isDisabled = false; }; const write$1 = ({ root, props }) => { const { isDisabled } = props; const shouldDisable = root.query('GET_DISABLED') || props.opacity === 0; if (shouldDisable && !isDisabled) { props.isDisabled = true; attr(root.element, 'disabled', 'disabled'); } else if (!shouldDisable && isDisabled) { props.isDisabled = false; root.element.removeAttribute('disabled'); } }; const fileActionButton = createView({ tag: 'button', attributes: { type: 'button', }, ignoreRect: true, ignoreRectUpdate: true, name: 'file-action-button', mixins: { apis: ['label'], styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'], animations: { scaleX: 'spring', scaleY: 'spring', translateX: 'spring', translateY: 'spring', opacity: { type: 'tween', duration: 250 }, }, listeners: true, }, create: create$1, write: write$1, }); const toNaturalFileSize = (bytes, decimalSeparator = '.', base = 1000, options = {}) => { const { labelBytes = 'bytes', labelKilobytes = 'KB', labelMegabytes = 'MB', labelGigabytes = 'GB', } = options; // no negative byte sizes bytes = Math.round(Math.abs(bytes)); const KB = base; const MB = base * base; const GB = base * base * base; // just bytes if (bytes < KB) { return `${bytes} ${labelBytes}`; } // kilobytes if (bytes < MB) { return `${Math.floor(bytes / KB)} ${labelKilobytes}`; } // megabytes if (bytes < GB) { return `${removeDecimalsWhenZero(bytes / MB, 1, decimalSeparator)} ${labelMegabytes}`; } // gigabytes return `${removeDecimalsWhenZero(bytes / GB, 2, decimalSeparator)} ${labelGigabytes}`; }; const removeDecimalsWhenZero = (value, decimalCount, separator) => { return value .toFixed(decimalCount) .split('.') .filter(part => part !== '0') .join(separator); }; const create$2 = ({ root, props }) => { // filename const fileName = createElement$1('span'); fileName.className = 'filepond--file-info-main'; // hide for screenreaders // the file is contained in a fieldset with legend that contains the filename // no need to read it twice attr(fileName, 'aria-hidden', 'true'); root.appendChild(fileName); root.ref.fileName = fileName; // filesize const fileSize = createElement$1('span'); fileSize.className = 'filepond--file-info-sub'; root.appendChild(fileSize); root.ref.fileSize = fileSize; // set initial values text(fileSize, root.query('GET_LABEL_FILE_WAITING_FOR_SIZE')); text(fileName, formatFilename(root.query('GET_ITEM_NAME', props.id))); }; const updateFile = ({ root, props }) => { text( root.ref.fileSize, toNaturalFileSize( root.query('GET_ITEM_SIZE', props.id), '.', root.query('GET_FILE_SIZE_BASE'), root.query('GET_FILE_SIZE_LABELS', root.query) ) ); text(root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id))); }; const updateFileSizeOnError = ({ root, props }) => { // if size is available don't fallback to unknown size message if (isInt(root.query('GET_ITEM_SIZE', props.id))) { updateFile({ root, props }); return; } text(root.ref.fileSize, root.query('GET_LABEL_FILE_SIZE_NOT_AVAILABLE')); }; const fileInfo = createView({ name: 'file-info', ignoreRect: true, ignoreRectUpdate: true, write: createRoute({ DID_LOAD_ITEM: updateFile, DID_UPDATE_ITEM_META: updateFile, DID_THROW_ITEM_LOAD_ERROR: updateFileSizeOnError, DID_THROW_ITEM_INVALID: updateFileSizeOnError, }), didCreateView: root => { applyFilters('CREATE_VIEW', { ...root, view: root }); }, create: create$2, mixins: { styles: ['translateX', 'translateY'], animations: { translateX: 'spring', translateY: 'spring', }, }, }); const toPercentage = value => Math.round(value * 100); const create$3 = ({ root }) => { // main status const main = createElement$1('span'); main.className = 'filepond--file-status-main'; root.appendChild(main); root.ref.main = main; // sub status const sub = createElement$1('span'); sub.className = 'filepond--file-status-sub'; root.appendChild(sub); root.ref.sub = sub; didSetItemLoadProgress({ root, action: { progress: null } }); }; const didSetItemLoadProgress = ({ root, action }) => { const title = action.progress === null ? root.query('GET_LABEL_FILE_LOADING') : `${root.query('GET_LABEL_FILE_LOADING')} ${toPercentage(action.progress)}%`; text(root.ref.main, title); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL')); }; const didSetItemProcessProgress = ({ root, action }) => { const title = action.progress === null ? root.query('GET_LABEL_FILE_PROCESSING') : `${root.query('GET_LABEL_FILE_PROCESSING')} ${toPercentage(action.progress)}%`; text(root.ref.main, title); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL')); }; const didRequestItemProcessing = ({ root }) => { text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING')); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL')); }; const didAbortItemProcessing = ({ root }) => { text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_ABORTED')); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_RETRY')); }; const didCompleteItemProcessing = ({ root }) => { text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_COMPLETE')); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_UNDO')); }; const clear = ({ root }) => { text(root.ref.main, ''); text(root.ref.sub, ''); }; const error = ({ root, action }) => { text(root.ref.main, action.status.main); text(root.ref.sub, action.status.sub); }; const fileStatus = createView({ name: 'file-status', ignoreRect: true, ignoreRectUpdate: true, write: createRoute({ DID_LOAD_ITEM: clear, DID_REVERT_ITEM_PROCESSING: clear, DID_REQUEST_ITEM_PROCESSING: didRequestItemProcessing, DID_ABORT_ITEM_PROCESSING: didAbortItemProcessing, DID_COMPLETE_ITEM_PROCESSING: didCompleteItemProcessing, DID_UPDATE_ITEM_PROCESS_PROGRESS: didSetItemProcessProgress, DID_UPDATE_ITEM_LOAD_PROGRESS: didSetItemLoadProgress, DID_THROW_ITEM_LOAD_ERROR: error, DID_THROW_ITEM_INVALID: error, DID_THROW_ITEM_PROCESSING_ERROR: error, DID_THROW_ITEM_PROCESSING_REVERT_ERROR: error, DID_THROW_ITEM_REMOVE_ERROR: error, }), didCreateView: root => { applyFilters('CREATE_VIEW', { ...root, view: root }); }, create: create$3, mixins: { styles: ['translateX', 'translateY', 'opacity'], animations: { opacity: { type: 'tween', duration: 250 }, translateX: 'spring', translateY: 'spring', }, }, }); /** * Button definitions for the file view */ const Buttons = { AbortItemLoad: { label: 'GET_LABEL_BUTTON_ABORT_ITEM_LOAD', action: 'ABORT_ITEM_LOAD', className: 'filepond--action-abort-item-load', align: 'LOAD_INDICATOR_POSITION', // right }, RetryItemLoad: { label: 'GET_LABEL_BUTTON_RETRY_ITEM_LOAD', action: 'RETRY_ITEM_LOAD', icon: 'GET_ICON_RETRY', className: 'filepond--action-retry-item-load', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, RemoveItem: { label: 'GET_LABEL_BUTTON_REMOVE_ITEM', action: 'REQUEST_REMOVE_ITEM', icon: 'GET_ICON_REMOVE', className: 'filepond--action-remove-item', align: 'BUTTON_REMOVE_ITEM_POSITION', // left }, ProcessItem: { label: 'GET_LABEL_BUTTON_PROCESS_ITEM', action: 'REQUEST_ITEM_PROCESSING', icon: 'GET_ICON_PROCESS', className: 'filepond--action-process-item', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, AbortItemProcessing: { label: 'GET_LABEL_BUTTON_ABORT_ITEM_PROCESSING', action: 'ABORT_ITEM_PROCESSING', className: 'filepond--action-abort-item-processing', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, RetryItemProcessing: { label: 'GET_LABEL_BUTTON_RETRY_ITEM_PROCESSING', action: 'RETRY_ITEM_PROCESSING', icon: 'GET_ICON_RETRY', className: 'filepond--action-retry-item-processing', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, RevertItemProcessing: { label: 'GET_LABEL_BUTTON_UNDO_ITEM_PROCESSING', action: 'REQUEST_REVERT_ITEM_PROCESSING', icon: 'GET_ICON_UNDO', className: 'filepond--action-revert-item-processing', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, }; // make a list of buttons, we can then remove buttons from this list if they're disabled const ButtonKeys = []; forin(Buttons, key => { ButtonKeys.push(key); }); const calculateFileInfoOffset = root => { if (getRemoveIndicatorAligment(root) === 'right') return 0; const buttonRect = root.ref.buttonRemoveItem.rect.element; return buttonRect.hidden ? null : buttonRect.width + buttonRect.left; }; const calculateButtonWidth = root => { const buttonRect = root.ref.buttonAbortItemLoad.rect.element; return buttonRect.width; }; // Force on full pixels so text stays crips const calculateFileVerticalCenterOffset = root => Math.floor(root.ref.buttonRemoveItem.rect.element.height / 4); const calculateFileHorizontalCenterOffset = root => Math.floor(root.ref.buttonRemoveItem.rect.element.left / 2); const getLoadIndicatorAlignment = root => root.query('GET_STYLE_LOAD_INDICATOR_POSITION'); const getProcessIndicatorAlignment = root => root.query('GET_STYLE_PROGRESS_INDICATOR_POSITION'); const getRemoveIndicatorAligment = root => root.query('GET_STYLE_BUTTON_REMOVE_ITEM_POSITION'); const DefaultStyle = { buttonAbortItemLoad: { opacity: 0 }, buttonRetryItemLoad: { opacity: 0 }, buttonRemoveItem: { opacity: 0 }, buttonProcessItem: { opacity: 0 }, buttonAbortItemProcessing: { opacity: 0 }, buttonRetryItemProcessing: { opacity: 0 }, buttonRevertItemProcessing: { opacity: 0 }, loadProgressIndicator: { opacity: 0, align: getLoadIndicatorAlignment }, processProgressIndicator: { opacity: 0, align: getProcessIndicatorAlignment }, processingCompleteIndicator: { opacity: 0, scaleX: 0.75, scaleY: 0.75 }, info: { translateX: 0, translateY: 0, opacity: 0 }, status: { translateX: 0, translateY: 0, opacity: 0 }, }; const IdleStyle = { buttonRemoveItem: { opacity: 1 }, buttonProcessItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { translateX: calculateFileInfoOffset }, }; const ProcessingStyle = { buttonAbortItemProcessing: { opacity: 1 }, processProgressIndicator: { opacity: 1 }, status: { opacity: 1 }, }; const StyleMap = { DID_THROW_ITEM_INVALID: { buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { translateX: calculateFileInfoOffset, opacity: 1 }, }, DID_START_ITEM_LOAD: { buttonAbortItemLoad: { opacity: 1 }, loadProgressIndicator: { opacity: 1 }, status: { opacity: 1 }, }, DID_THROW_ITEM_LOAD_ERROR: { buttonRetryItemLoad: { opacity: 1 }, buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 1 }, }, DID_START_ITEM_REMOVE: { processProgressIndicator: { opacity: 1, align: getRemoveIndicatorAligment }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 0 }, }, DID_THROW_ITEM_REMOVE_ERROR: { processProgressIndicator: { opacity: 0, align: getRemoveIndicatorAligment }, buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 1, translateX: calculateFileInfoOffset }, }, DID_LOAD_ITEM: IdleStyle, DID_LOAD_LOCAL_ITEM: { buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { translateX: calculateFileInfoOffset }, }, DID_START_ITEM_PROCESSING: ProcessingStyle, DID_REQUEST_ITEM_PROCESSING: ProcessingStyle, DID_UPDATE_ITEM_PROCESS_PROGRESS: ProcessingStyle, DID_COMPLETE_ITEM_PROCESSING: { buttonRevertItemProcessing: { opacity: 1 }, info: { opacity: 1 }, status: { opacity: 1 }, }, DID_THROW_ITEM_PROCESSING_ERROR: { buttonRemoveItem: { opacity: 1 }, buttonRetryItemProcessing: { opacity: 1 }, status: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, }, DID_THROW_ITEM_PROCESSING_REVERT_ERROR: { buttonRevertItemProcessing: { opacity: 1 }, status: { opacity: 1 }, info: { opacity: 1 }, }, DID_ABORT_ITEM_PROCESSING: { buttonRemoveItem: { opacity: 1 }, buttonProcessItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 1 }, }, DID_REVERT_ITEM_PROCESSING: IdleStyle, }; // complete indicator view const processingCompleteIndicatorView = createView({ create: ({ root }) => { root.element.innerHTML = root.query('GET_ICON_DONE'); }, name: 'processing-complete-indicator', ignoreRect: true, mixins: { styles: ['scaleX', 'scaleY', 'opacity'], animations: { scaleX: 'spring', scaleY: 'spring', opacity: { type: 'tween', duration: 250 }, }, }, }); /** * Creates the file view */ const create$4 = ({ root, props }) => { // copy Buttons object const LocalButtons = Object.keys(Buttons).reduce((prev, curr) => { prev[curr] = { ...Buttons[curr] }; return prev; }, {}); const { id } = props; // allow reverting upload const allowRevert = root.query('GET_ALLOW_REVERT'); // allow remove file const allowRemove = root.query('GET_ALLOW_REMOVE'); // allow processing upload const allowProcess = root.query('GET_ALLOW_PROCESS'); // is instant uploading, need this to determine the icon of the undo button const instantUpload = root.query('GET_INSTANT_UPLOAD'); // is async set up const isAsync = root.query('IS_ASYNC'); // should align remove item buttons const alignRemoveItemButton = root.query('GET_STYLE_BUTTON_REMOVE_ITEM_ALIGN'); // enabled buttons array let buttonFilter; if (isAsync) { if (allowProcess && !allowRevert) { // only remove revert button buttonFilter = key => !/RevertItemProcessing/.test(key); } else if (!allowProcess && allowRevert) { // only remove process button buttonFilter = key => !/ProcessItem|RetryItemProcessing|AbortItemProcessing/.test(key); } else if (!allowProcess && !allowRevert) { // remove all process buttons buttonFilter = key => !/Process/.test(key); } } else { // no process controls available buttonFilter = key => !/Process/.test(key); } const enabledButtons = buttonFilter ? ButtonKeys.filter(buttonFilter) : ButtonKeys.concat(); // update icon and label for revert button when instant uploading if (instantUpload && allowRevert) { LocalButtons['RevertItemProcessing'].label = 'GET_LABEL_BUTTON_REMOVE_ITEM'; LocalButtons['RevertItemProcessing'].icon = 'GET_ICON_REMOVE'; } // remove last button (revert) if not allowed if (isAsync && !allowRevert) { const map = StyleMap['DID_COMPLETE_ITEM_PROCESSING']; map.info.translateX = calculateFileHorizontalCenterOffset; map.info.translateY = calculateFileVerticalCenterOffset; map.status.translateY = calculateFileVerticalCenterOffset; map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 }; } // should align center if (isAsync && !allowProcess) { [ 'DID_START_ITEM_PROCESSING', 'DID_REQUEST_ITEM_PROCESSING', 'DID_UPDATE_ITEM_PROCESS_PROGRESS', 'DID_THROW_ITEM_PROCESSING_ERROR', ].forEach(key => { StyleMap[key].status.translateY = calculateFileVerticalCenterOffset; }); StyleMap['DID_THROW_ITEM_PROCESSING_ERROR'].status.translateX = calculateButtonWidth; } // move remove button to right if (alignRemoveItemButton && allowRevert) { LocalButtons['RevertItemProcessing'].align = 'BUTTON_REMOVE_ITEM_POSITION'; const map = StyleMap['DID_COMPLETE_ITEM_PROCESSING']; map.info.translateX = calculateFileInfoOffset; map.status.translateY = calculateFileVerticalCenterOffset; map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 }; } // show/hide RemoveItem button if (!allowRemove) { LocalButtons['RemoveItem'].disabled = true; } // create the button views forin(LocalButtons, (key, definition) => { // create button const buttonView = root.createChildView(fileActionButton, { label: root.query(definition.label), icon: root.query(definition.icon), opacity: 0, }); // should be appended? if (enabledButtons.includes(key)) { root.appendChildView(buttonView); } // toggle if (definition.disabled) { buttonView.element.setAttribute('disabled', 'disabled'); buttonView.element.setAttribute('hidden', 'hidden'); } // add position attribute buttonView.element.dataset.align = root.query(`GET_STYLE_${definition.align}`); // add class buttonView.element.classList.add(definition.className); // handle interactions buttonView.on('click', e => { e.stopPropagation(); if (definition.disabled) return; root.dispatch(definition.action, { query: id }); }); // set reference root.ref[`button${key}`] = buttonView; }); // checkmark root.ref.processingCompleteIndicator = root.appendChildView( root.createChildView(processingCompleteIndicatorView) ); root.ref.processingCompleteIndicator.element.dataset.align = root.query( `GET_STYLE_BUTTON_PROCESS_ITEM_POSITION` ); // create file info view root.ref.info = root.appendChildView(root.createChildView(fileInfo, { id })); // create file status view root.ref.status = root.appendChildView(root.createChildView(fileStatus, { id })); // add progress indicators const loadIndicatorView = root.appendChildView( root.createChildView(progressIndicator, { opacity: 0, align: root.query(`GET_STYLE_LOAD_INDICATOR_POSITION`), }) ); loadIndicatorView.element.classList.add('filepond--load-indicator'); root.ref.loadProgressIndicator = loadIndicatorView; const progressIndicatorView = root.appendChildView( root.createChildView(progressIndicator, { opacity: 0, align: root.query(`GET_STYLE_PROGRESS_INDICATOR_POSITION`), }) ); progressIndicatorView.element.classList.add('filepond--process-indicator'); root.ref.processProgressIndicator = progressIndicatorView; // current active styles root.ref.activeStyles = []; }; const write$2 = ({ root, actions, props }) => { // route actions route({ root, actions, props }); // select last state change action let action = actions .concat() .filter(action => /^DID_/.test(action.type)) .reverse() .find(action => StyleMap[action.type]); // a new action happened, let's get the matching styles if (action) { // define new active styles root.ref.activeStyles = []; const stylesToApply = StyleMap[action.type]; forin(DefaultStyle, (name, defaultStyles) => { // get reference to control const control = root.ref[name]; // loop over all styles for this control forin(defaultStyles, (key, defaultValue) => { const value = stylesToApply[name] && typeof stylesToApply[name][key] !== 'undefined' ? stylesToApply[name][key] : defaultValue; root.ref.activeStyles.push({ control, key, value }); }); }); } // apply active styles to element root.ref.activeStyles.forEach(({ control, key, value }) => { control[key] = typeof value === 'function' ? value(root) : value; }); }; const route = createRoute({ DID_SET_LABEL_BUTTON_ABORT_ITEM_PROCESSING: ({ root, action }) => { root.ref.buttonAbortItemProcessing.label = action.value; }, DID_SET_LABEL_BUTTON_ABORT_ITEM_LOAD: ({ root, action }) => { root.ref.buttonAbortItemLoad.label = action.value; }, DID_SET_LABEL_BUTTON_ABORT_ITEM_REMOVAL: ({ root, action }) => { root.ref.buttonAbortItemRemoval.label = action.value; }, DID_REQUEST_ITEM_PROCESSING: ({ root }) => { root.ref.processProgressIndicator.spin = true; root.ref.processProgressIndicator.progress = 0; }, DID_START_ITEM_LOAD: ({ root }) => { root.ref.loadProgressIndicator.spin = true; root.ref.loadProgressIndicator.progress = 0; }, DID_START_ITEM_REMOVE: ({ root }) => { root.ref.processProgressIndicator.spin = true; root.ref.processProgressIndicator.progress = 0; }, DID_UPDATE_ITEM_LOAD_PROGRESS: ({ root, action }) => { root.ref.loadProgressIndicator.spin = false; root.ref.loadProgressIndicator.progress = action.progress; }, DID_UPDATE_ITEM_PROCESS_PROGRESS: ({ root, action }) => { root.ref.processProgressIndicator.spin = false; root.ref.processProgressIndicator.progress = action.progress; }, }); const file = createView({ create: create$4, write: write$2, didCreateView: root => { applyFilters('CREATE_VIEW', { ...root, view: root }); }, name: 'file', }); /** * Creates the file view */ const create$5 = ({ root, props }) => { // filename root.ref.fileName = createElement$1('legend'); root.appendChild(root.ref.fileName); // file appended root.ref.file = root.appendChildView(root.createChildView(file, { id: props.id })); // data has moved to data.js root.ref.data = false; }; /** * Data storage */ const didLoadItem = ({ root, props }) => { // updates the legend of the fieldset so screenreaders can better group buttons text(root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id))); }; const fileWrapper = createView({ create: create$5, ignoreRect: true, write: createRoute({ DID_LOAD_ITEM: didLoadItem, }), didCreateView: root => { applyFilters('CREATE_VIEW', { ...root, view: root }); }, tag: 'fieldset', name: 'file-wrapper', }); const PANEL_SPRING_PROPS = { type: 'spring', damping: 0.6, mass: 7 }; const create$6 = ({ root, props }) => { [ { name: 'top', }, { name: 'center', props: { translateY: null, scaleY: null, }, mixins: { animations: { scaleY: PANEL_SPRING_PROPS, }, styles: ['translateY', 'scaleY'], }, }, { name: 'bottom', props: { translateY: null, }, mixins: { animations: { translateY: PANEL_SPRING_PROPS, }, styles: ['translateY'], }, }, ].forEach(section => { createSection(root, section, props.name); }); root.element.classList.add(`filepond--${props.name}`); root.ref.scalable = null; }; const createSection = (root, section, className) => { const viewConstructor = createView({ name: `panel-${section.name} filepond--${className}`, mixins: section.mixins, ignoreRectUpdate: true, }); const view = root.createChildView(viewConstructor, section.props); root.ref[section.name] = root.appendChildView(view); }; const write$3 = ({ root, props }) => { // update scalable state if (root.ref.scalable === null || props.scalable !== root.ref.scalable) { root.ref.scalable = isBoolean(props.scalable) ? props.scalable : true; root.element.dataset.scalable = root.ref.scalable; } // no height, can't set if (!props.height) return; // get child rects const topRect = root.ref.top.rect.element; const bottomRect = root.ref.bottom.rect.element; // make sure height never is smaller than bottom and top seciton heights combined (will probably never happen, but who knows) const height = Math.max(topRect.height + bottomRect.height, props.height); // offset center part root.ref.center.translateY = topRect.height; // scale center part // use math ceil to prevent transparent lines because of rounding errors root.ref.center.scaleY = (height - topRect.height - bottomRect.height) / 100; // offset bottom part root.ref.bottom.translateY = height - bottomRect.height; }; const panel = createView({ name: 'panel', read: ({ root, props }) => (props.heightCurrent = root.ref.bottom.translateY), write: write$3, create: create$6, ignoreRect: true, mixins: { apis: ['height', 'heightCurrent', 'scalable'], }, }); const createDragHelper = items => { const itemIds = items.map(item => item.id); let prevIndex = undefined; return { setIndex: index => { prevIndex = index; }, getIndex: () => prevIndex, getItemIndex: item => itemIds.indexOf(item.id), }; }; const ITEM_TRANSLATE_SPRING = { type: 'spring', stiffness: 0.75, damping: 0.45, mass: 10, }; const ITEM_SCALE_SPRING = 'spring'; const StateMap = { DID_START_ITEM_LOAD: 'busy', DID_UPDATE_ITEM_LOAD_PROGRESS: 'loading', DID_THROW_ITEM_INVALID: 'load-invalid', DID_THROW_ITEM_LOAD_ERROR: 'load-error', DID_LOAD_ITEM: 'idle', DID_THROW_ITEM_REMOVE_ERROR: 'remove-error', DID_START_ITEM_REMOVE: 'busy', DID_START_ITEM_PROCESSING: 'busy processing', DID_REQUEST_ITEM_PROCESSING: 'busy processing', DID_UPDATE_ITEM_PROCESS_PROGRESS: 'processing', DID_COMPLETE_ITEM_PROCESSING: 'processing-complete', DID_THROW_ITEM_PROCESSING_ERROR: 'processing-error', DID_THROW_ITEM_PROCESSING_REVERT_ERROR: 'processing-revert-error', DID_ABORT_ITEM_PROCESSING: 'cancelled', DID_REVERT_ITEM_PROCESSING: 'idle', }; /** * Creates the file view */ const create$7 = ({ root, props }) => { // select root.ref.handleClick = e => root.dispatch('DID_ACTIVATE_ITEM', { id: props.id }); // set id root.element.id = `filepond--item-${props.id}`; root.element.addEventListener('click', root.ref.handleClick); // file view root.ref.container = root.appendChildView(root.createChildView(fileWrapper, { id: props.id })); // file panel root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'item-panel' })); // default start height root.ref.panel.height = null; // by default not marked for removal props.markedForRemoval = false; // if not allowed to reorder file items, exit here if (!root.query('GET_ALLOW_REORDER')) return; // set to idle so shows grab cursor root.element.dataset.dragState = 'idle'; const grab = e => { if (!e.isPrimary) return; let removedActivateListener = false; const origin = { x: e.pageX, y: e.pageY, }; props.dragOrigin = { x: root.translateX, y: root.translateY, }; props.dragCenter = { x: e.offsetX, y: e.offsetY, }; const dragState = createDragHelper(root.query('GET_ACTIVE_ITEMS')); root.dispatch('DID_GRAB_ITEM', { id: props.id, dragState }); const drag = e => { if (!e.isPrimary) return; e.stopPropagation(); e.preventDefault(); props.dragOffset = { x: e.pageX - origin.x, y: e.pageY - origin.y, }; // if dragged stop listening to clicks, will re-add when done dragging const dist = props.dragOffset.x * props.dragOffset.x + props.dragOffset.y * props.dragOffset.y; if (dist > 16 && !removedActivateListener) { removedActivateListener = true; root.element.removeEventListener('click', root.ref.handleClick); } root.dispatch('DID_DRAG_ITEM', { id: props.id, dragState }); }; const drop = e => { if (!e.isPrimary) return; props.dragOffset = { x: e.pageX - origin.x, y: e.pageY - origin.y, }; reset(); }; const cancel = () => { reset(); }; const reset = () => { document.removeEventListener('pointercancel', cancel); document.removeEventListener('pointermove', drag); document.removeEventListener('pointerup', drop); root.dispatch('DID_DROP_ITEM', { id: props.id, dragState }); // start listening to clicks again if (removedActivateListener) { setTimeout(() => root.element.addEventListener('click', root.ref.handleClick), 0); } }; document.addEventListener('pointercancel', cancel); document.addEventListener('pointermove', drag); document.addEventListener('pointerup', drop); }; root.element.addEventListener('pointerdown', grab); }; const route$1 = createRoute({ DID_UPDATE_PANEL_HEIGHT: ({ root, action }) => { root.height = action.height; }, }); const write$4 = createRoute( { DID_GRAB_ITEM: ({ root, props }) => { props.dragOrigin = { x: root.translateX, y: root.translateY, }; }, DID_DRAG_ITEM: ({ root }) => { root.element.dataset.dragState = 'drag'; }, DID_DROP_ITEM: ({ root, props }) => { props.dragOffset = null; props.dragOrigin = null; root.element.dataset.dragState = 'drop'; }, }, ({ root, actions, props, shouldOptimize }) => { if (root.element.dataset.dragState === 'drop') { if (root.scaleX <= 1) { root.element.dataset.dragState = 'idle'; } } // select last state change action let action = actions .concat() .filter(action => /^DID_/.test(action.type)) .reverse() .find(action => StateMap[action.type]); // no need to set same state twice if (action && action.type !== props.currentState) { // set current state props.currentState = action.type; // set state root.element.dataset.filepondItemState = StateMap[props.currentState] || ''; } // route actions const aspectRatio = root.query('GET_ITEM_PANEL_ASPECT_RATIO') || root.query('GET_PANEL_ASPECT_RATIO'); if (!aspectRatio) { route$1({ root, actions, props }); if (!root.height && root.ref.container.rect.element.height > 0) { root.height = root.ref.container.rect.element.height; } } else if (!shouldOptimize) { root.height = root.rect.element.width * aspectRatio; } // sync panel height with item height if (shouldOptimize) { root.ref.panel.height = null; } root.ref.panel.height = root.height; } ); const item = createView({ create: create$7, write: write$4, destroy: ({ root, props }) => { root.element.removeEventListener('click', root.ref.handleClick); root.dispatch('RELEASE_ITEM', { query: props.id }); }, tag: 'li', name: 'item', mixins: { apis: [ 'id', 'interactionMethod', 'markedForRemoval', 'spawnDate', 'dragCenter', 'dragOrigin', 'dragOffset', ], styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity', 'height'], animations: { scaleX: ITEM_SCALE_SPRING, scaleY: ITEM_SCALE_SPRING, translateX: ITEM_TRANSLATE_SPRING, translateY: ITEM_TRANSLATE_SPRING, opacity: { type: 'tween', duration: 150 }, }, }, }); var getItemsPerRow = (horizontalSpace, itemWidth) => { // add one pixel leeway, when using percentages for item width total items can be 1.99 per row return Math.max(1, Math.floor((horizontalSpace + 1) / itemWidth)); }; const getItemIndexByPosition = (view, children, positionInView) => { if (!positionInView) return; const horizontalSpace = view.rect.element.width; // const children = view.childViews; const l = children.length; let last = null; // -1, don't move items to accomodate (either add to top or bottom) if (l === 0 || positionInView.top < children[0].rect.element.top) return -1; // let's get the item width const item = children[0]; const itemRect = item.rect.element; const itemHorizontalMargin = itemRect.marginLeft + itemRect.marginRight; const itemWidth = itemRect.width + itemHorizontalMargin; const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth); // stack if (itemsPerRow === 1) { for (let index = 0; index < l; index++) { const child = children[index]; const childMid = child.rect.outer.top + child.rect.element.height * 0.5; if (positionInView.top < childMid) { return index; } } return l; } // grid const itemVerticalMargin = itemRect.marginTop + itemRect.marginBottom; const itemHeight = itemRect.height + itemVerticalMargin; for (let index = 0; index < l; index++) { const indexX = index % itemsPerRow; const indexY = Math.floor(index / itemsPerRow); const offsetX = indexX * itemWidth; const offsetY = indexY * itemHeight; const itemTop = offsetY - itemRect.marginTop; const itemRight = offsetX + itemWidth; const itemBottom = offsetY + itemHeight + itemRect.marginBottom; if (positionInView.top < itemBottom && positionInView.top > itemTop) { if (positionInView.left < itemRight) { return index; } else if (index !== l - 1) { last = index; } else { last = null; } } } if (last !== null) { return last; } return l; }; const dropAreaDimensions = { height: 0, width: 0, get getHeight() { return this.height; }, set setHeight(val) { if (this.height === 0 || val === 0) this.height = val; }, get getWidth() { return this.width; }, set setWidth(val) { if (this.width === 0 || val === 0) this.width = val; }, setDimensions: function(height, width) { if (this.height === 0 || height === 0) this.height = height; if (this.width === 0 || width === 0) this.width = width; }, }; const create$8 = ({ root }) => { // need to set role to list as otherwise it won't be read as a list by VoiceOver attr(root.element, 'role', 'list'); root.ref.lastItemSpanwDate = Date.now(); }; /** * Inserts a new item * @param root * @param action */ const addItemView = ({ root, action }) => { const { id, index, interactionMethod } = action; root.ref.addIndex = index; const now = Date.now(); let spawnDate = now; let opacity = 1; if (interactionMethod !== InteractionMethod.NONE) { opacity = 0; const cooldown = root.query('GET_ITEM_INSERT_INTERVAL'); const dist = now - root.ref.lastItemSpanwDate; spawnDate = dist < cooldown ? now + (cooldown - dist) : now; } root.ref.lastItemSpanwDate = spawnDate; root.appendChildView( root.createChildView( // view type item, // props { spawnDate, id, opacity, interactionMethod, } ), index ); }; const moveItem = (item, x, y, vx = 0, vy = 1) => { // set to null to remove animation while dragging if (item.dragOffset) { item.translateX = null; item.translateY = null; item.translateX = item.dragOrigin.x + item.dragOffset.x; item.translateY = item.dragOrigin.y + item.dragOffset.y; item.scaleX = 1.025; item.scaleY = 1.025; } else { item.translateX = x; item.translateY = y; if (Date.now() > item.spawnDate) { // reveal element if (item.opacity === 0) { introItemView(item, x, y, vx, vy); } // make sure is default scale every frame item.scaleX = 1; item.scaleY = 1; item.opacity = 1; } } }; const introItemView = (item, x, y, vx, vy) => { if (item.interactionMethod === InteractionMethod.NONE) { item.translateX = null; item.translateX = x; item.translateY = null; item.translateY = y; } else if (item.interactionMethod === InteractionMethod.DROP) { item.translateX = null; item.translateX = x - vx * 20; item.translateY = null; item.translateY = y - vy * 10; item.scaleX = 0.8; item.scaleY = 0.8; } else if (item.interactionMethod === InteractionMethod.BROWSE) { item.translateY = null; item.translateY = y - 30; } else if (item.interactionMethod === InteractionMethod.API) { item.translateX = null; item.translateX = x - 30; item.translateY = null; } }; /** * Removes an existing item * @param root * @param action */ const removeItemView = ({ root, action }) => { const { id } = action; // get the view matching the given id const view = root.childViews.find(child => child.id === id); // if no view found, exit if (!view) { return; } // animate view out of view view.scaleX = 0.9; view.scaleY = 0.9; view.opacity = 0; // mark for removal view.markedForRemoval = true; }; const getItemHeight = child => child.rect.element.height + child.rect.element.marginBottom + child.rect.element.marginTop; const getItemWidth = child => child.rect.element.width + child.rect.element.marginLeft * 0.5 + child.rect.element.marginRight * 0.5; const dragItem = ({ root, action }) => { const { id, dragState } = action; // reference to item const item = root.query('GET_ITEM', { id }); // get the view matching the given id const view = root.childViews.find(child => child.id === id); const numItems = root.childViews.length; const oldIndex = dragState.getItemIndex(item); // if no view found, exit if (!view) return; const dragPosition = { x: view.dragOrigin.x + view.dragOffset.x + view.dragCenter.x, y: view.dragOrigin.y + view.dragOffset.y + view.dragCenter.y, }; // get drag area dimensions const dragHeight = getItemHeight(view); const dragWidth = getItemWidth(view); // get rows and columns (There will always be at least one row and one column if a file is present) let cols = Math.floor(root.rect.outer.width / dragWidth); if (cols > numItems) cols = numItems; // rows are used to find when we have left the preview area bounding box const rows = Math.floor(numItems / cols + 1); dropAreaDimensions.setHeight = dragHeight * rows; dropAreaDimensions.setWidth = dragWidth * cols; // get new index of dragged item var location = { y: Math.floor(dragPosition.y / dragHeight), x: Math.floor(dragPosition.x / dragWidth), getGridIndex: function getGridIndex() { if ( dragPosition.y > dropAreaDimensions.getHeight || dragPosition.y < 0 || dragPosition.x > dropAreaDimensions.getWidth || dragPosition.x < 0 ) return oldIndex; return this.y * cols + this.x; }, getColIndex: function getColIndex() { const items = root.query('GET_ACTIVE_ITEMS'); const visibleChildren = root.childViews.filter(child => child.rect.element.height); const children = items.map(item => visibleChildren.find(childView => childView.id === item.id) ); const currentIndex = children.findIndex(child => child === view); const dragHeight = getItemHeight(view); const l = children.length; let idx = l; let childHeight = 0; let childBottom = 0; let childTop = 0; for (let i = 0; i < l; i++) { childHeight = getItemHeight(children[i]); childTop = childBottom; childBottom = childTop + childHeight; if (dragPosition.y < childBottom) { if (currentIndex > i) { if (dragPosition.y < childTop + dragHeight) { idx = i; break; } continue; } idx = i; break; } } return idx; }, }; // get new index const index = cols > 1 ? location.getGridIndex() : location.getColIndex(); root.dispatch('MOVE_ITEM', { query: view, index }); // if the index of the item changed, dispatch reorder action const currentIndex = dragState.getIndex(); if (currentIndex === undefined || currentIndex !== index) { dragState.setIndex(index); if (currentIndex === undefined) return; root.dispatch('DID_REORDER_ITEMS', { items: root.query('GET_ACTIVE_ITEMS'), origin: oldIndex, target: index, }); } }; /** * Setup action routes */ const route$2 = createRoute({ DID_ADD_ITEM: addItemView, DID_REMOVE_ITEM: removeItemView, DID_DRAG_ITEM: dragItem, }); /** * Write to view * @param root * @param actions * @param props */ const write$5 = ({ root, props, actions, shouldOptimize }) => { // route actions route$2({ root, props, actions }); const { dragCoordinates } = props; // available space on horizontal axis const horizontalSpace = root.rect.element.width; // only draw children that have dimensions const visibleChildren = root.childViews.filter(child => child.rect.element.height); // sort based on current active items const children = root .query('GET_ACTIVE_ITEMS') .map(item => visibleChildren.find(child => child.id === item.id)) .filter(item => item); // get index const dragIndex = dragCoordinates ? getItemIndexByPosition(root, children, dragCoordinates) : null; // add index is used to reserve the dropped/added item index till the actual item is rendered const addIndex = root.ref.addIndex || null; // add index no longer needed till possibly next draw root.ref.addIndex = null; let dragIndexOffset = 0; let removeIndexOffset = 0; let addIndexOffset = 0; if (children.length === 0) return; const childRect = children[0].rect.element; const itemVerticalMargin = childRect.marginTop + childRect.marginBottom; const itemHorizontalMargin = childRect.marginLeft + childRect.marginRight; const itemWidth = childRect.width + itemHorizontalMargin; const itemHeight = childRect.height + itemVerticalMargin; const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth); // stack if (itemsPerRow === 1) { let offsetY = 0; let dragOffset = 0; children.forEach((child, index) => { if (dragIndex) { let dist = index - dragIndex; if (dist === -2) { dragOffset = -itemVerticalMargin * 0.25; } else if (dist === -1) { dragOffset = -itemVerticalMargin * 0.75; } else if (dist === 0) { dragOffset = itemVerticalMargin * 0.75; } else if (dist === 1) { dragOffset = itemVerticalMargin * 0.25; } else { dragOffset = 0; } } if (shouldOptimize) { child.translateX = null; child.translateY = null; } if (!child.markedForRemoval) { moveItem(child, 0, offsetY + dragOffset); } let itemHeight = child.rect.element.height + itemVerticalMargin; let visualHeight = itemHeight * (child.markedForRemoval ? child.opacity : 1); offsetY += visualHeight; }); } // grid else { let prevX = 0; let prevY = 0; children.forEach((child, index) => { if (index === dragIndex) { dragIndexOffset = 1; } if (index === addIndex) { addIndexOffset += 1; } if (child.markedForRemoval && child.opacity < 0.5) { removeIndexOffset -= 1; } const visualIndex = index + addIndexOffset + dragIndexOffset + removeIndexOffset; const indexX = visualIndex % itemsPerRow; const indexY = Math.floor(visualIndex / itemsPerRow); const offsetX = indexX * itemWidth; const offsetY = indexY * itemHeight; const vectorX = Math.sign(offsetX - prevX); const vectorY = Math.sign(offsetY - prevY); prevX = offsetX; prevY = offsetY; if (child.markedForRemoval) return; if (shouldOptimize) { child.translateX = null; child.translateY = null; } moveItem(child, offsetX, offsetY, vectorX, vectorY); }); } }; /** * Filters actions that are meant specifically for a certain child of the list * @param child * @param actions */ const filterSetItemActions = (child, actions) => actions.filter(action => { // if action has an id, filter out actions that don't have this child id if (action.data && action.data.id) { return child.id === action.data.id; } // allow all other actions return true; }); const list = createView({ create: create$8, write: write$5, tag: 'ul', name: 'list', didWriteView: ({ root }) => { root.childViews .filter(view => view.markedForRemoval && view.opacity === 0 && view.resting) .forEach(view => { view._destroy(); root.removeChildView(view); }); }, filterFrameActionsForChild: filterSetItemActions, mixins: { apis: ['dragCoordinates'], }, }); const create$9 = ({ root, props }) => { root.ref.list = root.appendChildView(root.createChildView(list)); props.dragCoordinates = null; props.overflowing = false; }; const storeDragCoordinates = ({ root, props, action }) => { if (!root.query('GET_ITEM_INSERT_LOCATION_FREEDOM')) return; props.dragCoordinates = { left: action.position.scopeLeft - root.ref.list.rect.element.left, top: action.position.scopeTop - (root.rect.outer.top + root.rect.element.marginTop + root.rect.element.scrollTop), }; }; const clearDragCoordinates = ({ props }) => { props.dragCoordinates = null; }; const route$3 = createRoute({ DID_DRAG: storeDragCoordinates, DID_END_DRAG: clearDragCoordinates, }); const write$6 = ({ root, props, actions }) => { // route actions route$3({ root, props, actions }); // current drag position root.ref.list.dragCoordinates = props.dragCoordinates; // if currently overflowing but no longer received overflow if (props.overflowing && !props.overflow) { props.overflowing = false; // reset overflow state root.element.dataset.state = ''; root.height = null; } // if is not overflowing currently but does receive overflow value if (props.overflow) { const newHeight = Math.round(props.overflow); if (newHeight !== root.height) { props.overflowing = true; root.element.dataset.state = 'overflow'; root.height = newHeight; } } }; const listScroller = createView({ create: create$9, write: write$6, name: 'list-scroller', mixins: { apis: ['overflow', 'dragCoordinates'], styles: ['height', 'translateY'], animations: { translateY: 'spring', }, }, }); const attrToggle = (element, name, state, enabledValue = '') => { if (state) { attr(element, name, enabledValue); } else { element.removeAttribute(name); } }; const resetFileInput = input => { // no value, no need to reset if (!input || input.value === '') { return; } try { // for modern browsers input.value = ''; } catch (err) {} // for IE10 if (input.value) { // quickly append input to temp form and reset form const form = createElement$1('form'); const parentNode = input.parentNode; const ref = input.nextSibling; form.appendChild(input); form.reset(); // re-inject input where it originally was if (ref) { parentNode.insertBefore(input, ref); } else { parentNode.appendChild(input); } } }; const create$a = ({ root, props }) => { // set id so can be referenced from outside labels root.element.id = `filepond--browser-${props.id}`; // set name of element (is removed when a value is set) attr(root.element, 'name', root.query('GET_NAME')); // we have to link this element to the status element attr(root.element, 'aria-controls', `filepond--assistant-${props.id}`); // set label, we use labelled by as otherwise the screenreader does not read the "browse" text in the label (as it has tabindex: 0) attr(root.element, 'aria-labelledby', `filepond--drop-label-${props.id}`); // set configurable props setAcceptedFileTypes({ root, action: { value: root.query('GET_ACCEPTED_FILE_TYPES') } }); toggleAllowMultiple({ root, action: { value: root.query('GET_ALLOW_MULTIPLE') } }); toggleDirectoryFilter({ root, action: { value: root.query('GET_ALLOW_DIRECTORIES_ONLY') } }); toggleDisabled({ root }); toggleRequired({ root, action: { value: root.query('GET_REQUIRED') } }); setCaptureMethod({ root, action: { value: root.query('GET_CAPTURE_METHOD') } }); // handle changes to the input field root.ref.handleChange = e => { if (!root.element.value) { return; } // extract files and move value of webkitRelativePath path to _relativePath const files = Array.from(root.element.files).map(file => { file._relativePath = file.webkitRelativePath; return file; }); // we add a little delay so the OS file select window can move out of the way before we add our file setTimeout(() => { // load files props.onload(files); // reset input, it's just for exposing a method to drop files, should not retain any state resetFileInput(root.element); }, 250); }; root.element.addEventListener('change', root.ref.handleChange); }; const setAcceptedFileTypes = ({ root, action }) => { if (!root.query('GET_ALLOW_SYNC_ACCEPT_ATTRIBUTE')) return; attrToggle(root.element, 'accept', !!action.value, action.value ? action.value.join(',') : ''); }; const toggleAllowMultiple = ({ root, action }) => { attrToggle(root.element, 'multiple', action.value); }; const toggleDirectoryFilter = ({ root, action }) => { attrToggle(root.element, 'webkitdirectory', action.value); }; const toggleDisabled = ({ root }) => { const isDisabled = root.query('GET_DISABLED'); const doesAllowBrowse = root.query('GET_ALLOW_BROWSE'); const disableField = isDisabled || !doesAllowBrowse; attrToggle(root.element, 'disabled', disableField); }; const toggleRequired = ({ root, action }) => { // want to remove required, always possible if (!action.value) { attrToggle(root.element, 'required', false); } // if want to make required, only possible when zero items else if (root.query('GET_TOTAL_ITEMS') === 0) { attrToggle(root.element, 'required', true); } }; const setCaptureMethod = ({ root, action }) => { attrToggle(root.element, 'capture', !!action.value, action.value === true ? '' : action.value); }; const updateRequiredStatus = ({ root }) => { const { element } = root; // always remove the required attribute when more than zero items if (root.query('GET_TOTAL_ITEMS') > 0) { attrToggle(element, 'required', false); attrToggle(element, 'name', false); // still has items const activeItems = root.query('GET_ACTIVE_ITEMS'); let hasInvalidField = false; for (let i = 0; i < activeItems.length; i++) { if (activeItems[i].status === ItemStatus.LOAD_ERROR) { hasInvalidField = true; } } // set validity status root.element.setCustomValidity( hasInvalidField ? root.query('GET_LABEL_INVALID_FIELD') : '' ); } else { // add name attribute attrToggle(element, 'name', true, root.query('GET_NAME')); // remove any validation messages const shouldCheckValidity = root.query('GET_CHECK_VALIDITY'); if (shouldCheckValidity) { element.setCustomValidity(''); } // we only add required if the field has been deemed required if (root.query('GET_REQUIRED')) { attrToggle(element, 'required', true); } } }; const updateFieldValidityStatus = ({ root }) => { const shouldCheckValidity = root.query('GET_CHECK_VALIDITY'); if (!shouldCheckValidity) return; root.element.setCustomValidity(root.query('GET_LABEL_INVALID_FIELD')); }; const browser = createView({ tag: 'input', name: 'browser', ignoreRect: true, ignoreRectUpdate: true, attributes: { type: 'file', }, create: create$a, destroy: ({ root }) => { root.element.removeEventListener('change', root.ref.handleChange); }, write: createRoute({ DID_LOAD_ITEM: updateRequiredStatus, DID_REMOVE_ITEM: updateRequiredStatus, DID_THROW_ITEM_INVALID: updateFieldValidityStatus, DID_SET_DISABLED: toggleDisabled, DID_SET_ALLOW_BROWSE: toggleDisabled, DID_SET_ALLOW_DIRECTORIES_ONLY: toggleDirectoryFilter, DID_SET_ALLOW_MULTIPLE: toggleAllowMultiple, DID_SET_ACCEPTED_FILE_TYPES: setAcceptedFileTypes, DID_SET_CAPTURE_METHOD: setCaptureMethod, DID_SET_REQUIRED: toggleRequired, }), }); const Key = { ENTER: 13, SPACE: 32, }; const create$b = ({ root, props }) => { // create the label and link it to the file browser const label = createElement$1('label'); attr(label, 'for', `filepond--browser-${props.id}`); // use for labeling file input (aria-labelledby on file input) attr(label, 'id', `filepond--drop-label-${props.id}`); // handle keys root.ref.handleKeyDown = e => { const isActivationKey = e.keyCode === Key.ENTER || e.keyCode === Key.SPACE; if (!isActivationKey) return; // stops from triggering the element a second time e.preventDefault(); // click link (will then in turn activate file input) root.ref.label.click(); }; root.ref.handleClick = e => { const isLabelClick = e.target === label || label.contains(e.target); // don't want to click twice if (isLabelClick) return; // click link (will then in turn activate file input) root.ref.label.click(); }; // attach events label.addEventListener('keydown', root.ref.handleKeyDown); root.element.addEventListener('click', root.ref.handleClick); // update updateLabelValue(label, props.caption); // add! root.appendChild(label); root.ref.label = label; }; const updateLabelValue = (label, value) => { label.innerHTML = value; const clickable = label.querySelector('.filepond--label-action'); if (clickable) { attr(clickable, 'tabindex', '0'); } return value; }; const dropLabel = createView({ name: 'drop-label', ignoreRect: true, create: create$b, destroy: ({ root }) => { root.ref.label.addEventListener('keydown', root.ref.handleKeyDown); root.element.removeEventListener('click', root.ref.handleClick); }, write: createRoute({ DID_SET_LABEL_IDLE: ({ root, action }) => { updateLabelValue(root.ref.label, action.value); }, }), mixins: { styles: ['opacity', 'translateX', 'translateY'], animations: { opacity: { type: 'tween', duration: 150 }, translateX: 'spring', translateY: 'spring', }, }, }); const blob = createView({ name: 'drip-blob', ignoreRect: true, mixins: { styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'], animations: { scaleX: 'spring', scaleY: 'spring', translateX: 'spring', translateY: 'spring', opacity: { type: 'tween', duration: 250 }, }, }, }); const addBlob = ({ root }) => { const centerX = root.rect.element.width * 0.5; const centerY = root.rect.element.height * 0.5; root.ref.blob = root.appendChildView( root.createChildView(blob, { opacity: 0, scaleX: 2.5, scaleY: 2.5, translateX: centerX, translateY: centerY, }) ); }; const moveBlob = ({ root, action }) => { if (!root.ref.blob) { addBlob({ root }); return; } root.ref.blob.translateX = action.position.scopeLeft; root.ref.blob.translateY = action.position.scopeTop; root.ref.blob.scaleX = 1; root.ref.blob.scaleY = 1; root.ref.blob.opacity = 1; }; const hideBlob = ({ root }) => { if (!root.ref.blob) { return; } root.ref.blob.opacity = 0; }; const explodeBlob = ({ root }) => { if (!root.ref.blob) { return; } root.ref.blob.scaleX = 2.5; root.ref.blob.scaleY = 2.5; root.ref.blob.opacity = 0; }; const write$7 = ({ root, props, actions }) => { route$4({ root, props, actions }); const { blob } = root.ref; if (actions.length === 0 && blob && blob.opacity === 0) { root.removeChildView(blob); root.ref.blob = null; } }; const route$4 = createRoute({ DID_DRAG: moveBlob, DID_DROP: explodeBlob, DID_END_DRAG: hideBlob, }); const drip = createView({ ignoreRect: true, ignoreRectUpdate: true, name: 'drip', write: write$7, }); const setInputFiles = (element, files) => { try { // Create a DataTransfer instance and add a newly created file const dataTransfer = new DataTransfer(); files.forEach(file => { if (file instanceof File) { dataTransfer.items.add(file); } else { dataTransfer.items.add( new File([file], file.name, { type: file.type, }) ); } }); // Assign the DataTransfer files list to the file input element.files = dataTransfer.files; } catch (err) { return false; } return true; }; const create$c = ({ root }) => { root.ref.fields = {}; const legend = document.createElement('legend'); legend.textContent = 'Files'; root.element.appendChild(legend); }; const getField = (root, id) => root.ref.fields[id]; const syncFieldPositionsWithItems = root => { root.query('GET_ACTIVE_ITEMS').forEach(item => { if (!root.ref.fields[item.id]) return; root.element.appendChild(root.ref.fields[item.id]); }); }; const didReorderItems = ({ root }) => syncFieldPositionsWithItems(root); const didAddItem = ({ root, action }) => { const fileItem = root.query('GET_ITEM', action.id); const isLocalFile = fileItem.origin === FileOrigin.LOCAL; const shouldUseFileInput = !isLocalFile && root.query('SHOULD_UPDATE_FILE_INPUT'); const dataContainer = createElement$1('input'); dataContainer.type = shouldUseFileInput ? 'file' : 'hidden'; dataContainer.name = root.query('GET_NAME'); root.ref.fields[action.id] = dataContainer; syncFieldPositionsWithItems(root); }; const didLoadItem$1 = ({ root, action }) => { const field = getField(root, action.id); if (!field) return; // store server ref in hidden input if (action.serverFileReference !== null) field.value = action.serverFileReference; // store file item in file input if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return; const fileItem = root.query('GET_ITEM', action.id); setInputFiles(field, [fileItem.file]); }; const didPrepareOutput = ({ root, action }) => { // this timeout pushes the handler after 'load' if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return; setTimeout(() => { const field = getField(root, action.id); if (!field) return; setInputFiles(field, [action.file]); }, 0); }; const didSetDisabled = ({ root }) => { root.element.disabled = root.query('GET_DISABLED'); }; const didRemoveItem = ({ root, action }) => { const field = getField(root, action.id); if (!field) return; if (field.parentNode) field.parentNode.removeChild(field); delete root.ref.fields[action.id]; }; // only runs for server files. will refuse to update the value if the field // is a file field const didDefineValue = ({ root, action }) => { const field = getField(root, action.id); if (!field) return; if (action.value === null) { // clear field value field.removeAttribute('value'); } else { // set field value if (field.type != 'file') { field.value = action.value; } } syncFieldPositionsWithItems(root); }; const write$8 = createRoute({ DID_SET_DISABLED: didSetDisabled, DID_ADD_ITEM: didAddItem, DID_LOAD_ITEM: didLoadItem$1, DID_REMOVE_ITEM: didRemoveItem, DID_DEFINE_VALUE: didDefineValue, DID_PREPARE_OUTPUT: didPrepareOutput, DID_REORDER_ITEMS: didReorderItems, DID_SORT_ITEMS: didReorderItems, }); const data = createView({ tag: 'fieldset', name: 'data', create: create$c, write: write$8, ignoreRect: true, }); const getRootNode = element => ('getRootNode' in element ? element.getRootNode() : document); const images = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'tiff']; const text$1 = ['css', 'csv', 'html', 'txt']; const map = { zip: 'zip|compressed', epub: 'application/epub+zip', }; const guesstimateMimeType = (extension = '') => { extension = extension.toLowerCase(); if (images.includes(extension)) { return ( 'image/' + (extension === 'jpg' ? 'jpeg' : extension === 'svg' ? 'svg+xml' : extension) ); } if (text$1.includes(extension)) { return 'text/' + extension; } return map[extension] || ''; }; const requestDataTransferItems = dataTransfer => new Promise((resolve, reject) => { // try to get links from transfer, if found we'll exit immediately (unless a file is in the dataTransfer as well, this is because Firefox could represent the file as a URL and a file object at the same time) const links = getLinks(dataTransfer); if (links.length && !hasFiles(dataTransfer)) { return resolve(links); } // try to get files from the transfer getFiles(dataTransfer).then(resolve); }); /** * Test if datatransfer has files */ const hasFiles = dataTransfer => { if (dataTransfer.files) return dataTransfer.files.length > 0; return false; }; /** * Extracts files from a DataTransfer object */ const getFiles = dataTransfer => new Promise((resolve, reject) => { // get the transfer items as promises const promisedFiles = (dataTransfer.items ? Array.from(dataTransfer.items) : []) // only keep file system items (files and directories) .filter(item => isFileSystemItem(item)) // map each item to promise .map(item => getFilesFromItem(item)); // if is empty, see if we can extract some info from the files property as a fallback if (!promisedFiles.length) { // TODO: test for directories (should not be allowed) // Use FileReader, problem is that the files property gets lost in the process resolve(dataTransfer.files ? Array.from(dataTransfer.files) : []); return; } // done! Promise.all(promisedFiles) .then(returnedFileGroups => { // flatten groups const files = []; returnedFileGroups.forEach(group => { files.push.apply(files, group); }); // done (filter out empty files)! resolve( files .filter(file => file) .map(file => { if (!file._relativePath) file._relativePath = file.webkitRelativePath; return file; }) ); }) .catch(console.error); }); const isFileSystemItem = item => { if (isEntry(item)) { const entry = getAsEntry(item); if (entry) { return entry.isFile || entry.isDirectory; } } return item.kind === 'file'; }; const getFilesFromItem = item => new Promise((resolve, reject) => { if (isDirectoryEntry(item)) { getFilesInDirectory(getAsEntry(item)) .then(resolve) .catch(reject); return; } resolve([item.getAsFile()]); }); const getFilesInDirectory = entry => new Promise((resolve, reject) => { const files = []; // the total entries to read let dirCounter = 0; let fileCounter = 0; const resolveIfDone = () => { if (fileCounter === 0 && dirCounter === 0) { resolve(files); } }; // the recursive function const readEntries = dirEntry => { dirCounter++; const directoryReader = dirEntry.createReader(); // directories are returned in batches, we need to process all batches before we're done const readBatch = () => { directoryReader.readEntries(entries => { if (entries.length === 0) { dirCounter--; resolveIfDone(); return; } entries.forEach(entry => { // recursively read more directories if (entry.isDirectory) { readEntries(entry); } else { // read as file fileCounter++; entry.file(file => { const correctedFile = correctMissingFileType(file); if (entry.fullPath) correctedFile._relativePath = entry.fullPath; files.push(correctedFile); fileCounter--; resolveIfDone(); }); } }); // try to get next batch of files readBatch(); }, reject); }; // read first batch of files readBatch(); }; // go! readEntries(entry); }); const correctMissingFileType = file => { if (file.type.length) return file; const date = file.lastModifiedDate; const name = file.name; const type = guesstimateMimeType(getExtensionFromFilename(file.name)); if (!type.length) return file; file = file.slice(0, file.size, type); file.name = name; file.lastModifiedDate = date; return file; }; const isDirectoryEntry = item => isEntry(item) && (getAsEntry(item) || {}).isDirectory; const isEntry = item => 'webkitGetAsEntry' in item; const getAsEntry = item => item.webkitGetAsEntry(); /** * Extracts links from a DataTransfer object */ const getLinks = dataTransfer => { let links = []; try { // look in meta data property links = getLinksFromTransferMetaData(dataTransfer); if (links.length) { return links; } links = getLinksFromTransferURLData(dataTransfer); } catch (e) { // nope nope nope (probably IE trouble) } return links; }; const getLinksFromTransferURLData = dataTransfer => { let data = dataTransfer.getData('url'); if (typeof data === 'string' && data.length) { return [data]; } return []; }; const getLinksFromTransferMetaData = dataTransfer => { let data = dataTransfer.getData('text/html'); if (typeof data === 'string' && data.length) { const matches = data.match(/src\s*=\s*"(.+?)"/); if (matches) { return [matches[1]]; } } return []; }; const dragNDropObservers = []; const eventPosition = e => ({ pageLeft: e.pageX, pageTop: e.pageY, scopeLeft: e.offsetX || e.layerX, scopeTop: e.offsetY || e.layerY, }); const createDragNDropClient = (element, scopeToObserve, filterElement) => { const observer = getDragNDropObserver(scopeToObserve); const client = { element, filterElement, state: null, ondrop: () => {}, onenter: () => {}, ondrag: () => {}, onexit: () => {}, onload: () => {}, allowdrop: () => {}, }; client.destroy = observer.addListener(client); return client; }; const getDragNDropObserver = element => { // see if already exists, if so, return const observer = dragNDropObservers.find(item => item.element === element); if (observer) { return observer; } // create new observer, does not yet exist for this element const newObserver = createDragNDropObserver(element); dragNDropObservers.push(newObserver); return newObserver; }; const createDragNDropObserver = element => { const clients = []; const routes = { dragenter, dragover, dragleave, drop, }; const handlers = {}; forin(routes, (event, createHandler) => { handlers[event] = createHandler(element, clients); element.addEventListener(event, handlers[event], false); }); const observer = { element, addListener: client => { // add as client clients.push(client); // return removeListener function return () => { // remove client clients.splice(clients.indexOf(client), 1); // if no more clients, clean up observer if (clients.length === 0) { dragNDropObservers.splice(dragNDropObservers.indexOf(observer), 1); forin(routes, event => { element.removeEventListener(event, handlers[event], false); }); } }; }, }; return observer; }; const elementFromPoint = (root, point) => { if (!('elementFromPoint' in root)) { root = document; } return root.elementFromPoint(point.x, point.y); }; const isEventTarget = (e, target) => { // get root const root = getRootNode(target); // get element at position // if root is not actual shadow DOM and does not have elementFromPoint method, use the one on document const elementAtPosition = elementFromPoint(root, { x: e.pageX - window.pageXOffset, y: e.pageY - window.pageYOffset, }); // test if target is the element or if one of its children is return elementAtPosition === target || target.contains(elementAtPosition); }; let initialTarget = null; const setDropEffect = (dataTransfer, effect) => { // is in try catch as IE11 will throw error if not try { dataTransfer.dropEffect = effect; } catch (e) {} }; const dragenter = (root, clients) => e => { e.preventDefault(); initialTarget = e.target; clients.forEach(client => { const { element, onenter } = client; if (isEventTarget(e, element)) { client.state = 'enter'; // fire enter event onenter(eventPosition(e)); } }); }; const dragover = (root, clients) => e => { e.preventDefault(); const dataTransfer = e.dataTransfer; requestDataTransferItems(dataTransfer).then(items => { let overDropTarget = false; clients.some(client => { const { filterElement, element, onenter, onexit, ondrag, allowdrop } = client; // by default we can drop setDropEffect(dataTransfer, 'copy'); // allow transfer of these items const allowsTransfer = allowdrop(items); // only used when can be dropped on page if (!allowsTransfer) { setDropEffect(dataTransfer, 'none'); return; } // targetting this client if (isEventTarget(e, element)) { overDropTarget = true; // had no previous state, means we are entering this client if (client.state === null) { client.state = 'enter'; onenter(eventPosition(e)); return; } // now over element (no matter if it allows the drop or not) client.state = 'over'; // needs to allow transfer if (filterElement && !allowsTransfer) { setDropEffect(dataTransfer, 'none'); return; } // dragging ondrag(eventPosition(e)); } else { // should be over an element to drop if (filterElement && !overDropTarget) { setDropEffect(dataTransfer, 'none'); } // might have just left this client? if (client.state) { client.state = null; onexit(eventPosition(e)); } } }); }); }; const drop = (root, clients) => e => { e.preventDefault(); const dataTransfer = e.dataTransfer; requestDataTransferItems(dataTransfer).then(items => { clients.forEach(client => { const { filterElement, element, ondrop, onexit, allowdrop } = client; client.state = null; // if we're filtering on element we need to be over the element to drop if (filterElement && !isEventTarget(e, element)) return; // no transfer for this client if (!allowdrop(items)) return onexit(eventPosition(e)); // we can drop these items on this client ondrop(eventPosition(e), items); }); }); }; const dragleave = (root, clients) => e => { if (initialTarget !== e.target) { return; } clients.forEach(client => { const { onexit } = client; client.state = null; onexit(eventPosition(e)); }); }; const createHopper = (scope, validateItems, options) => { // is now hopper scope scope.classList.add('filepond--hopper'); // shortcuts const { catchesDropsOnPage, requiresDropOnElement, filterItems = items => items } = options; // create a dnd client const client = createDragNDropClient( scope, catchesDropsOnPage ? document.documentElement : scope, requiresDropOnElement ); // current client state let lastState = ''; let currentState = ''; // determines if a file may be dropped client.allowdrop = items => { // TODO: if we can, throw error to indicate the items cannot by dropped return validateItems(filterItems(items)); }; client.ondrop = (position, items) => { const filteredItems = filterItems(items); if (!validateItems(filteredItems)) { api.ondragend(position); return; } currentState = 'drag-drop'; api.onload(filteredItems, position); }; client.ondrag = position => { api.ondrag(position); }; client.onenter = position => { currentState = 'drag-over'; api.ondragstart(position); }; client.onexit = position => { currentState = 'drag-exit'; api.ondragend(position); }; const api = { updateHopperState: () => { if (lastState !== currentState) { scope.dataset.hopperState = currentState; lastState = currentState; } }, onload: () => {}, ondragstart: () => {}, ondrag: () => {}, ondragend: () => {}, destroy: () => { // destroy client client.destroy(); }, }; return api; }; let listening = false; const listeners$1 = []; const handlePaste = e => { // if is pasting in input or textarea and the target is outside of a filepond scope, ignore const activeEl = document.activeElement; const isActiveElementEditable = activeEl && (/textarea|input/i.test(activeEl.nodeName) || activeEl.getAttribute('contenteditable') === 'true' || activeEl.getAttribute('contenteditable') === ''); if (isActiveElementEditable) { // test textarea or input is contained in filepond root let inScope = false; let element = activeEl; while (element !== document.body) { if (element.classList.contains('filepond--root')) { inScope = true; break; } element = element.parentNode; } if (!inScope) return; } requestDataTransferItems(e.clipboardData).then(files => { // no files received if (!files.length) { return; } // notify listeners of received files listeners$1.forEach(listener => listener(files)); }); }; const listen = cb => { // can't add twice if (listeners$1.includes(cb)) { return; } // add initial listener listeners$1.push(cb); // setup paste listener for entire page if (listening) { return; } listening = true; document.addEventListener('paste', handlePaste); }; const unlisten = listener => { arrayRemove(listeners$1, listeners$1.indexOf(listener)); // clean up if (listeners$1.length === 0) { document.removeEventListener('paste', handlePaste); listening = false; } }; const createPaster = () => { const cb = files => { api.onload(files); }; const api = { destroy: () => { unlisten(cb); }, onload: () => {}, }; listen(cb); return api; }; /** * Creates the file view */ const create$d = ({ root, props }) => { root.element.id = `filepond--assistant-${props.id}`; attr(root.element, 'role', 'alert'); attr(root.element, 'aria-live', 'polite'); attr(root.element, 'aria-relevant', 'additions'); }; let addFilesNotificationTimeout = null; let notificationClearTimeout = null; const filenames = []; const assist = (root, message) => { root.element.textContent = message; }; const clear$1 = root => { root.element.textContent = ''; }; const listModified = (root, filename, label) => { const total = root.query('GET_TOTAL_ITEMS'); assist( root, `${label} ${filename}, ${total} ${ total === 1 ? root.query('GET_LABEL_FILE_COUNT_SINGULAR') : root.query('GET_LABEL_FILE_COUNT_PLURAL') }` ); // clear group after set amount of time so the status is not read twice clearTimeout(notificationClearTimeout); notificationClearTimeout = setTimeout(() => { clear$1(root); }, 1500); }; const isUsingFilePond = root => root.element.parentNode.contains(document.activeElement); const itemAdded = ({ root, action }) => { if (!isUsingFilePond(root)) { return; } root.element.textContent = ''; const item = root.query('GET_ITEM', action.id); filenames.push(item.filename); clearTimeout(addFilesNotificationTimeout); addFilesNotificationTimeout = setTimeout(() => { listModified(root, filenames.join(', '), root.query('GET_LABEL_FILE_ADDED')); filenames.length = 0; }, 750); }; const itemRemoved = ({ root, action }) => { if (!isUsingFilePond(root)) { return; } const item = action.item; listModified(root, item.filename, root.query('GET_LABEL_FILE_REMOVED')); }; const itemProcessed = ({ root, action }) => { // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file const item = root.query('GET_ITEM', action.id); const filename = item.filename; const label = root.query('GET_LABEL_FILE_PROCESSING_COMPLETE'); assist(root, `${filename} ${label}`); }; const itemProcessedUndo = ({ root, action }) => { const item = root.query('GET_ITEM', action.id); const filename = item.filename; const label = root.query('GET_LABEL_FILE_PROCESSING_ABORTED'); assist(root, `${filename} ${label}`); }; const itemError = ({ root, action }) => { const item = root.query('GET_ITEM', action.id); const filename = item.filename; // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file assist(root, `${action.status.main} ${filename} ${action.status.sub}`); }; const assistant = createView({ create: create$d, ignoreRect: true, ignoreRectUpdate: true, write: createRoute({ DID_LOAD_ITEM: itemAdded, DID_REMOVE_ITEM: itemRemoved, DID_COMPLETE_ITEM_PROCESSING: itemProcessed, DID_ABORT_ITEM_PROCESSING: itemProcessedUndo, DID_REVERT_ITEM_PROCESSING: itemProcessedUndo, DID_THROW_ITEM_REMOVE_ERROR: itemError, DID_THROW_ITEM_LOAD_ERROR: itemError, DID_THROW_ITEM_INVALID: itemError, DID_THROW_ITEM_PROCESSING_ERROR: itemError, }), tag: 'span', name: 'assistant', }); const toCamels = (string, separator = '-') => string.replace(new RegExp(`${separator}.`, 'g'), sub => sub.charAt(1).toUpperCase()); const debounce = (func, interval = 16, immidiateOnly = true) => { let last = Date.now(); let timeout = null; return (...args) => { clearTimeout(timeout); const dist = Date.now() - last; const fn = () => { last = Date.now(); func(...args); }; if (dist < interval) { // we need to delay by the difference between interval and dist // for example: if distance is 10 ms and interval is 16 ms, // we need to wait an additional 6ms before calling the function) if (!immidiateOnly) { timeout = setTimeout(fn, interval - dist); } } else { // go! fn(); } }; }; const MAX_FILES_LIMIT = 1000000; const prevent = e => e.preventDefault(); const create$e = ({ root, props }) => { // Add id const id = root.query('GET_ID'); if (id) { root.element.id = id; } // Add className const className = root.query('GET_CLASS_NAME'); if (className) { className .split(' ') .filter(name => name.length) .forEach(name => { root.element.classList.add(name); }); } // Field label root.ref.label = root.appendChildView( root.createChildView(dropLabel, { ...props, translateY: null, caption: root.query('GET_LABEL_IDLE'), }) ); // List of items root.ref.list = root.appendChildView(root.createChildView(listScroller, { translateY: null })); // Background panel root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'panel-root' })); // Assistant notifies assistive tech when content changes root.ref.assistant = root.appendChildView(root.createChildView(assistant, { ...props })); // Data root.ref.data = root.appendChildView(root.createChildView(data, { ...props })); // Measure (tests if fixed height was set) // DOCTYPE needs to be set for this to work root.ref.measure = createElement$1('div'); root.ref.measure.style.height = '100%'; root.element.appendChild(root.ref.measure); // information on the root height or fixed height status root.ref.bounds = null; // apply initial style properties root.query('GET_STYLES') .filter(style => !isEmpty(style.value)) .map(({ name, value }) => { root.element.dataset[name] = value; }); // determine if width changed root.ref.widthPrevious = null; root.ref.widthUpdated = debounce(() => { root.ref.updateHistory = []; root.dispatch('DID_RESIZE_ROOT'); }, 250); // history of updates root.ref.previousAspectRatio = null; root.ref.updateHistory = []; // prevent scrolling and zooming on iOS (only if supports pointer events, for then we can enable reorder) const canHover = window.matchMedia('(pointer: fine) and (hover: hover)').matches; const hasPointerEvents = 'PointerEvent' in window; if (root.query('GET_ALLOW_REORDER') && hasPointerEvents && !canHover) { root.element.addEventListener('touchmove', prevent, { passive: false }); root.element.addEventListener('gesturestart', prevent); } // add credits const credits = root.query('GET_CREDITS'); const hasCredits = credits.length === 2; if (hasCredits) { const frag = document.createElement('a'); frag.className = 'filepond--credits'; frag.href = credits[0]; frag.tabIndex = -1; frag.target = '_blank'; frag.rel = 'noopener noreferrer nofollow'; frag.textContent = credits[1]; root.element.appendChild(frag); root.ref.credits = frag; } }; const write$9 = ({ root, props, actions }) => { // route actions route$5({ root, props, actions }); // apply style properties actions .filter(action => /^DID_SET_STYLE_/.test(action.type)) .filter(action => !isEmpty(action.data.value)) .map(({ type, data }) => { const name = toCamels(type.substring(8).toLowerCase(), '_'); root.element.dataset[name] = data.value; root.invalidateLayout(); }); if (root.rect.element.hidden) return; if (root.rect.element.width !== root.ref.widthPrevious) { root.ref.widthPrevious = root.rect.element.width; root.ref.widthUpdated(); } // get box bounds, we do this only once let bounds = root.ref.bounds; if (!bounds) { bounds = root.ref.bounds = calculateRootBoundingBoxHeight(root); // destroy measure element root.element.removeChild(root.ref.measure); root.ref.measure = null; } // get quick references to various high level parts of the upload tool const { hopper, label, list, panel } = root.ref; // sets correct state to hopper scope if (hopper) { hopper.updateHopperState(); } // bool to indicate if we're full or not const aspectRatio = root.query('GET_PANEL_ASPECT_RATIO'); const isMultiItem = root.query('GET_ALLOW_MULTIPLE'); const totalItems = root.query('GET_TOTAL_ITEMS'); const maxItems = isMultiItem ? root.query('GET_MAX_FILES') || MAX_FILES_LIMIT : 1; const atMaxCapacity = totalItems === maxItems; // action used to add item const addAction = actions.find(action => action.type === 'DID_ADD_ITEM'); // if reached max capacity and we've just reached it if (atMaxCapacity && addAction) { // get interaction type const interactionMethod = addAction.data.interactionMethod; // hide label label.opacity = 0; if (isMultiItem) { label.translateY = -40; } else { if (interactionMethod === InteractionMethod.API) { label.translateX = 40; } else if (interactionMethod === InteractionMethod.BROWSE) { label.translateY = 40; } else { label.translateY = 30; } } } else if (!atMaxCapacity) { label.opacity = 1; label.translateX = 0; label.translateY = 0; } const listItemMargin = calculateListItemMargin(root); const listHeight = calculateListHeight(root); const labelHeight = label.rect.element.height; const currentLabelHeight = !isMultiItem || atMaxCapacity ? 0 : labelHeight; const listMarginTop = atMaxCapacity ? list.rect.element.marginTop : 0; const listMarginBottom = totalItems === 0 ? 0 : list.rect.element.marginBottom; const visualHeight = currentLabelHeight + listMarginTop + listHeight.visual + listMarginBottom; const boundsHeight = currentLabelHeight + listMarginTop + listHeight.bounds + listMarginBottom; // link list to label bottom position list.translateY = Math.max(0, currentLabelHeight - list.rect.element.marginTop) - listItemMargin.top; if (aspectRatio) { // fixed aspect ratio // calculate height based on width const width = root.rect.element.width; const height = width * aspectRatio; // clear history if aspect ratio has changed if (aspectRatio !== root.ref.previousAspectRatio) { root.ref.previousAspectRatio = aspectRatio; root.ref.updateHistory = []; } // remember this width const history = root.ref.updateHistory; history.push(width); const MAX_BOUNCES = 2; if (history.length > MAX_BOUNCES * 2) { const l = history.length; const bottom = l - 10; let bounces = 0; for (let i = l; i >= bottom; i--) { if (history[i] === history[i - 2]) { bounces++; } if (bounces >= MAX_BOUNCES) { // dont adjust height return; } } } // fix height of panel so it adheres to aspect ratio panel.scalable = false; panel.height = height; // available height for list const listAvailableHeight = // the height of the panel minus the label height height - currentLabelHeight - // the room we leave open between the end of the list and the panel bottom (listMarginBottom - listItemMargin.bottom) - // if we're full we need to leave some room between the top of the panel and the list (atMaxCapacity ? listMarginTop : 0); if (listHeight.visual > listAvailableHeight) { list.overflow = listAvailableHeight; } else { list.overflow = null; } // set container bounds (so pushes siblings downwards) root.height = height; } else if (bounds.fixedHeight) { // fixed height // fix height of panel panel.scalable = false; // available height for list const listAvailableHeight = // the height of the panel minus the label height bounds.fixedHeight - currentLabelHeight - // the room we leave open between the end of the list and the panel bottom (listMarginBottom - listItemMargin.bottom) - // if we're full we need to leave some room between the top of the panel and the list (atMaxCapacity ? listMarginTop : 0); // set list height if (listHeight.visual > listAvailableHeight) { list.overflow = listAvailableHeight; } else { list.overflow = null; } // no need to set container bounds as these are handles by CSS fixed height } else if (bounds.cappedHeight) { // max-height // not a fixed height panel const isCappedHeight = visualHeight >= bounds.cappedHeight; const panelHeight = Math.min(bounds.cappedHeight, visualHeight); panel.scalable = true; panel.height = isCappedHeight ? panelHeight : panelHeight - listItemMargin.top - listItemMargin.bottom; // available height for list const listAvailableHeight = // the height of the panel minus the label height panelHeight - currentLabelHeight - // the room we leave open between the end of the list and the panel bottom (listMarginBottom - listItemMargin.bottom) - // if we're full we need to leave some room between the top of the panel and the list (atMaxCapacity ? listMarginTop : 0); // set list height (if is overflowing) if (visualHeight > bounds.cappedHeight && listHeight.visual > listAvailableHeight) { list.overflow = listAvailableHeight; } else { list.overflow = null; } // set container bounds (so pushes siblings downwards) root.height = Math.min( bounds.cappedHeight, boundsHeight - listItemMargin.top - listItemMargin.bottom ); } else { // flexible height // not a fixed height panel const itemMargin = totalItems > 0 ? listItemMargin.top + listItemMargin.bottom : 0; panel.scalable = true; panel.height = Math.max(labelHeight, visualHeight - itemMargin); // set container bounds (so pushes siblings downwards) root.height = Math.max(labelHeight, boundsHeight - itemMargin); } // move credits to bottom if (root.ref.credits && panel.heightCurrent) root.ref.credits.style.transform = `translateY(${panel.heightCurrent}px)`; }; const calculateListItemMargin = root => { const item = root.ref.list.childViews[0].childViews[0]; return item ? { top: item.rect.element.marginTop, bottom: item.rect.element.marginBottom, } : { top: 0, bottom: 0, }; }; const calculateListHeight = root => { let visual = 0; let bounds = 0; // get file list reference const scrollList = root.ref.list; const itemList = scrollList.childViews[0]; const visibleChildren = itemList.childViews.filter(child => child.rect.element.height); const children = root .query('GET_ACTIVE_ITEMS') .map(item => visibleChildren.find(child => child.id === item.id)) .filter(item => item); // no children, done! if (children.length === 0) return { visual, bounds }; const horizontalSpace = itemList.rect.element.width; const dragIndex = getItemIndexByPosition(itemList, children, scrollList.dragCoordinates); const childRect = children[0].rect.element; const itemVerticalMargin = childRect.marginTop + childRect.marginBottom; const itemHorizontalMargin = childRect.marginLeft + childRect.marginRight; const itemWidth = childRect.width + itemHorizontalMargin; const itemHeight = childRect.height + itemVerticalMargin; const newItem = typeof dragIndex !== 'undefined' && dragIndex >= 0 ? 1 : 0; const removedItem = children.find(child => child.markedForRemoval && child.opacity < 0.45) ? -1 : 0; const verticalItemCount = children.length + newItem + removedItem; const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth); // stack if (itemsPerRow === 1) { children.forEach(item => { const height = item.rect.element.height + itemVerticalMargin; bounds += height; visual += height * item.opacity; }); } // grid else { bounds = Math.ceil(verticalItemCount / itemsPerRow) * itemHeight; visual = bounds; } return { visual, bounds }; }; const calculateRootBoundingBoxHeight = root => { const height = root.ref.measureHeight || null; const cappedHeight = parseInt(root.style.maxHeight, 10) || null; const fixedHeight = height === 0 ? null : height; return { cappedHeight, fixedHeight, }; }; const exceedsMaxFiles = (root, items) => { const allowReplace = root.query('GET_ALLOW_REPLACE'); const allowMultiple = root.query('GET_ALLOW_MULTIPLE'); const totalItems = root.query('GET_TOTAL_ITEMS'); let maxItems = root.query('GET_MAX_FILES'); // total amount of items being dragged const totalBrowseItems = items.length; // if does not allow multiple items and dragging more than one item if (!allowMultiple && totalBrowseItems > 1) { root.dispatch('DID_THROW_MAX_FILES', { source: items, error: createResponse('warning', 0, 'Max files'), }); return true; } // limit max items to one if not allowed to drop multiple items maxItems = allowMultiple ? maxItems : 1; if (!allowMultiple && allowReplace) { // There is only one item, so there is room to replace or add an item return false; } // no more room? const hasMaxItems = isInt(maxItems); if (hasMaxItems && totalItems + totalBrowseItems > maxItems) { root.dispatch('DID_THROW_MAX_FILES', { source: items, error: createResponse('warning', 0, 'Max files'), }); return true; } return false; }; const getDragIndex = (list, children, position) => { const itemList = list.childViews[0]; return getItemIndexByPosition(itemList, children, { left: position.scopeLeft - itemList.rect.element.left, top: position.scopeTop - (list.rect.outer.top + list.rect.element.marginTop + list.rect.element.scrollTop), }); }; /** * Enable or disable file drop functionality */ const toggleDrop = root => { const isAllowed = root.query('GET_ALLOW_DROP'); const isDisabled = root.query('GET_DISABLED'); const enabled = isAllowed && !isDisabled; if (enabled && !root.ref.hopper) { const hopper = createHopper( root.element, items => { // allow quick validation of dropped items const beforeDropFile = root.query('GET_BEFORE_DROP_FILE') || (() => true); // all items should be validated by all filters as valid const dropValidation = root.query('GET_DROP_VALIDATION'); return dropValidation ? items.every( item => applyFilters('ALLOW_HOPPER_ITEM', item, { query: root.query, }).every(result => result === true) && beforeDropFile(item) ) : true; }, { filterItems: items => { const ignoredFiles = root.query('GET_IGNORED_FILES'); return items.filter(item => { if (isFile(item)) { return !ignoredFiles.includes(item.name.toLowerCase()); } return true; }); }, catchesDropsOnPage: root.query('GET_DROP_ON_PAGE'), requiresDropOnElement: root.query('GET_DROP_ON_ELEMENT'), } ); hopper.onload = (items, position) => { // get item children elements and sort based on list sort const list = root.ref.list.childViews[0]; const visibleChildren = list.childViews.filter(child => child.rect.element.height); const children = root .query('GET_ACTIVE_ITEMS') .map(item => visibleChildren.find(child => child.id === item.id)) .filter(item => item); applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(queue => { // these files don't fit so stop here if (exceedsMaxFiles(root, queue)) return false; // go root.dispatch('ADD_ITEMS', { items: queue, index: getDragIndex(root.ref.list, children, position), interactionMethod: InteractionMethod.DROP, }); }); root.dispatch('DID_DROP', { position }); root.dispatch('DID_END_DRAG', { position }); }; hopper.ondragstart = position => { root.dispatch('DID_START_DRAG', { position }); }; hopper.ondrag = debounce(position => { root.dispatch('DID_DRAG', { position }); }); hopper.ondragend = position => { root.dispatch('DID_END_DRAG', { position }); }; root.ref.hopper = hopper; root.ref.drip = root.appendChildView(root.createChildView(drip)); } else if (!enabled && root.ref.hopper) { root.ref.hopper.destroy(); root.ref.hopper = null; root.removeChildView(root.ref.drip); } }; /** * Enable or disable browse functionality */ const toggleBrowse = (root, props) => { const isAllowed = root.query('GET_ALLOW_BROWSE'); const isDisabled = root.query('GET_DISABLED'); const enabled = isAllowed && !isDisabled; if (enabled && !root.ref.browser) { root.ref.browser = root.appendChildView( root.createChildView(browser, { ...props, onload: items => { applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch, }).then(queue => { // these files don't fit so stop here if (exceedsMaxFiles(root, queue)) return false; // add items! root.dispatch('ADD_ITEMS', { items: queue, index: -1, interactionMethod: InteractionMethod.BROWSE, }); }); }, }), 0 ); } else if (!enabled && root.ref.browser) { root.removeChildView(root.ref.browser); root.ref.browser = null; } }; /** * Enable or disable paste functionality */ const togglePaste = root => { const isAllowed = root.query('GET_ALLOW_PASTE'); const isDisabled = root.query('GET_DISABLED'); const enabled = isAllowed && !isDisabled; if (enabled && !root.ref.paster) { root.ref.paster = createPaster(); root.ref.paster.onload = items => { applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(queue => { // these files don't fit so stop here if (exceedsMaxFiles(root, queue)) return false; // add items! root.dispatch('ADD_ITEMS', { items: queue, index: -1, interactionMethod: InteractionMethod.PASTE, }); }); }; } else if (!enabled && root.ref.paster) { root.ref.paster.destroy(); root.ref.paster = null; } }; /** * Route actions */ const route$5 = createRoute({ DID_SET_ALLOW_BROWSE: ({ root, props }) => { toggleBrowse(root, props); }, DID_SET_ALLOW_DROP: ({ root }) => { toggleDrop(root); }, DID_SET_ALLOW_PASTE: ({ root }) => { togglePaste(root); }, DID_SET_DISABLED: ({ root, props }) => { toggleDrop(root); togglePaste(root); toggleBrowse(root, props); const isDisabled = root.query('GET_DISABLED'); if (isDisabled) { root.element.dataset.disabled = 'disabled'; } else { // delete root.element.dataset.disabled; <= this does not work on iOS 10 root.element.removeAttribute('data-disabled'); } }, }); const root = createView({ name: 'root', read: ({ root }) => { if (root.ref.measure) { root.ref.measureHeight = root.ref.measure.offsetHeight; } }, create: create$e, write: write$9, destroy: ({ root }) => { if (root.ref.paster) { root.ref.paster.destroy(); } if (root.ref.hopper) { root.ref.hopper.destroy(); } root.element.removeEventListener('touchmove', prevent); root.element.removeEventListener('gesturestart', prevent); }, mixins: { styles: ['height'], }, }); // creates the app const createApp = (initialOptions = {}) => { // let element let originalElement = null; // get default options const defaultOptions = getOptions(); // create the data store, this will contain all our app info const store = createStore( // initial state (should be serializable) createInitialState(defaultOptions), // queries [queries, createOptionQueries(defaultOptions)], // action handlers [actions, createOptionActions(defaultOptions)] ); // set initial options store.dispatch('SET_OPTIONS', { options: initialOptions }); // kick thread if visibility changes const visibilityHandler = () => { if (document.hidden) return; store.dispatch('KICK'); }; document.addEventListener('visibilitychange', visibilityHandler); // re-render on window resize start and finish let resizeDoneTimer = null; let isResizing = false; let isResizingHorizontally = false; let initialWindowWidth = null; let currentWindowWidth = null; const resizeHandler = () => { if (!isResizing) { isResizing = true; } clearTimeout(resizeDoneTimer); resizeDoneTimer = setTimeout(() => { isResizing = false; initialWindowWidth = null; currentWindowWidth = null; if (isResizingHorizontally) { isResizingHorizontally = false; store.dispatch('DID_STOP_RESIZE'); } }, 500); }; window.addEventListener('resize', resizeHandler); // render initial view const view = root(store, { id: getUniqueId() }); // // PRIVATE API ------------------------------------------------------------------------------------- // let isResting = false; let isHidden = false; const readWriteApi = { // necessary for update loop /** * Reads from dom (never call manually) * @private */ _read: () => { // test if we're resizing horizontally // TODO: see if we can optimize this by measuring root rect if (isResizing) { currentWindowWidth = window.innerWidth; if (!initialWindowWidth) { initialWindowWidth = currentWindowWidth; } if (!isResizingHorizontally && currentWindowWidth !== initialWindowWidth) { store.dispatch('DID_START_RESIZE'); isResizingHorizontally = true; } } if (isHidden && isResting) { // test if is no longer hidden isResting = view.element.offsetParent === null; } // if resting, no need to read as numbers will still all be correct if (isResting) return; // read view data view._read(); // if is hidden we need to know so we exit rest mode when revealed isHidden = view.rect.element.hidden; }, /** * Writes to dom (never call manually) * @private */ _write: ts => { // get all actions from store const actions = store .processActionQueue() // filter out set actions (these will automatically trigger DID_SET) .filter(action => !/^SET_/.test(action.type)); // if was idling and no actions stop here if (isResting && !actions.length) return; // some actions might trigger events routeActionsToEvents(actions); // update the view isResting = view._write(ts, actions, isResizingHorizontally); // will clean up all archived items removeReleasedItems(store.query('GET_ITEMS')); // now idling if (isResting) { store.processDispatchQueue(); } }, }; // // EXPOSE EVENTS ------------------------------------------------------------------------------------- // const createEvent = name => data => { // create default event const event = { type: name, }; // no data to add if (!data) { return event; } // copy relevant props if (data.hasOwnProperty('error')) { event.error = data.error ? { ...data.error } : null; } if (data.status) { event.status = { ...data.status }; } if (data.file) { event.output = data.file; } // only source is available, else add item if possible if (data.source) { event.file = data.source; } else if (data.item || data.id) { const item = data.item ? data.item : store.query('GET_ITEM', data.id); event.file = item ? createItemAPI(item) : null; } // map all items in a possible items array if (data.items) { event.items = data.items.map(createItemAPI); } // if this is a progress event add the progress amount if (/progress/.test(name)) { event.progress = data.progress; } // copy relevant props if (data.hasOwnProperty('origin') && data.hasOwnProperty('target')) { event.origin = data.origin; event.target = data.target; } return event; }; const eventRoutes = { DID_DESTROY: createEvent('destroy'), DID_INIT: createEvent('init'), DID_THROW_MAX_FILES: createEvent('warning'), DID_INIT_ITEM: createEvent('initfile'), DID_START_ITEM_LOAD: createEvent('addfilestart'), DID_UPDATE_ITEM_LOAD_PROGRESS: createEvent('addfileprogress'), DID_LOAD_ITEM: createEvent('addfile'), DID_THROW_ITEM_INVALID: [createEvent('error'), createEvent('addfile')], DID_THROW_ITEM_LOAD_ERROR: [createEvent('error'), createEvent('addfile')], DID_THROW_ITEM_REMOVE_ERROR: [createEvent('error'), createEvent('removefile')], DID_PREPARE_OUTPUT: createEvent('preparefile'), DID_START_ITEM_PROCESSING: createEvent('processfilestart'), DID_UPDATE_ITEM_PROCESS_PROGRESS: createEvent('processfileprogress'), DID_ABORT_ITEM_PROCESSING: createEvent('processfileabort'), DID_COMPLETE_ITEM_PROCESSING: createEvent('processfile'), DID_COMPLETE_ITEM_PROCESSING_ALL: createEvent('processfiles'), DID_REVERT_ITEM_PROCESSING: createEvent('processfilerevert'), DID_THROW_ITEM_PROCESSING_ERROR: [createEvent('error'), createEvent('processfile')], DID_REMOVE_ITEM: createEvent('removefile'), DID_UPDATE_ITEMS: createEvent('updatefiles'), DID_ACTIVATE_ITEM: createEvent('activatefile'), DID_REORDER_ITEMS: createEvent('reorderfiles'), }; const exposeEvent = event => { // create event object to be dispatched const detail = { pond: exports, ...event }; delete detail.type; view.element.dispatchEvent( new CustomEvent(`FilePond:${event.type}`, { // event info detail, // event behaviour bubbles: true, cancelable: true, composed: true, // triggers listeners outside of shadow root }) ); // event object to params used for `on()` event handlers and callbacks `oninit()` const params = []; // if is possible error event, make it the first param if (event.hasOwnProperty('error')) { params.push(event.error); } // file is always section if (event.hasOwnProperty('file')) { params.push(event.file); } // append other props const filtered = ['type', 'error', 'file']; Object.keys(event) .filter(key => !filtered.includes(key)) .forEach(key => params.push(event[key])); // on(type, () => { }) exports.fire(event.type, ...params); // oninit = () => {} const handler = store.query(`GET_ON${event.type.toUpperCase()}`); if (handler) { handler(...params); } }; const routeActionsToEvents = actions => { if (!actions.length) return; actions .filter(action => eventRoutes[action.type]) .forEach(action => { const routes = eventRoutes[action.type]; (Array.isArray(routes) ? routes : [routes]).forEach(route => { // this isn't fantastic, but because of the stacking of settimeouts plugins can handle the did_load before the did_init if (action.type === 'DID_INIT_ITEM') { exposeEvent(route(action.data)); } else { setTimeout(() => { exposeEvent(route(action.data)); }, 0); } }); }); }; // // PUBLIC API ------------------------------------------------------------------------------------- // const setOptions = options => store.dispatch('SET_OPTIONS', { options }); const getFile = query => store.query('GET_ACTIVE_ITEM', query); const prepareFile = query => new Promise((resolve, reject) => { store.dispatch('REQUEST_ITEM_PREPARE', { query, success: item => { resolve(item); }, failure: error => { reject(error); }, }); }); const addFile = (source, options = {}) => new Promise((resolve, reject) => { addFiles([{ source, options }], { index: options.index }) .then(items => resolve(items && items[0])) .catch(reject); }); const isFilePondFile = obj => obj.file && obj.id; const removeFile = (query, options) => { // if only passed options if (typeof query === 'object' && !isFilePondFile(query) && !options) { options = query; query = undefined; } // request item removal store.dispatch('REMOVE_ITEM', { ...options, query }); // see if item has been removed return store.query('GET_ACTIVE_ITEM', query) === null; }; const addFiles = (...args) => new Promise((resolve, reject) => { const sources = []; const options = {}; // user passed a sources array if (isArray(args[0])) { sources.push.apply(sources, args[0]); Object.assign(options, args[1] || {}); } else { // user passed sources as arguments, last one might be options object const lastArgument = args[args.length - 1]; if (typeof lastArgument === 'object' && !(lastArgument instanceof Blob)) { Object.assign(options, args.pop()); } // add rest to sources sources.push(...args); } store.dispatch('ADD_ITEMS', { items: sources, index: options.index, interactionMethod: InteractionMethod.API, success: resolve, failure: reject, }); }); const getFiles = () => store.query('GET_ACTIVE_ITEMS'); const processFile = query => new Promise((resolve, reject) => { store.dispatch('REQUEST_ITEM_PROCESSING', { query, success: item => { resolve(item); }, failure: error => { reject(error); }, }); }); const prepareFiles = (...args) => { const queries = Array.isArray(args[0]) ? args[0] : args; const items = queries.length ? queries : getFiles(); return Promise.all(items.map(prepareFile)); }; const processFiles = (...args) => { const queries = Array.isArray(args[0]) ? args[0] : args; if (!queries.length) { const files = getFiles().filter( item => !(item.status === ItemStatus.IDLE && item.origin === FileOrigin.LOCAL) && item.status !== ItemStatus.PROCESSING && item.status !== ItemStatus.PROCESSING_COMPLETE && item.status !== ItemStatus.PROCESSING_REVERT_ERROR ); return Promise.all(files.map(processFile)); } return Promise.all(queries.map(processFile)); }; const removeFiles = (...args) => { const queries = Array.isArray(args[0]) ? args[0] : args; let options; if (typeof queries[queries.length - 1] === 'object') { options = queries.pop(); } else if (Array.isArray(args[0])) { options = args[1]; } const files = getFiles(); if (!queries.length) return Promise.all(files.map(file => removeFile(file, options))); // when removing by index the indexes shift after each file removal so we need to convert indexes to ids const mappedQueries = queries .map(query => (isNumber(query) ? (files[query] ? files[query].id : null) : query)) .filter(query => query); return mappedQueries.map(q => removeFile(q, options)); }; const exports = { // supports events ...on(), // inject private api methods ...readWriteApi, // inject all getters and setters ...createOptionAPI(store, defaultOptions), /** * Override options defined in options object * @param options */ setOptions, /** * Load the given file * @param source - the source of the file (either a File, base64 data uri or url) * @param options - object, { index: 0 } */ addFile, /** * Load the given files * @param sources - the sources of the files to load * @param options - object, { index: 0 } */ addFiles, /** * Returns the file objects matching the given query * @param query { string, number, null } */ getFile, /** * Upload file with given name * @param query { string, number, null } */ processFile, /** * Request prepare output for file with given name * @param query { string, number, null } */ prepareFile, /** * Removes a file by its name * @param query { string, number, null } */ removeFile, /** * Moves a file to a new location in the files list */ moveFile: (query, index) => store.dispatch('MOVE_ITEM', { query, index }), /** * Returns all files (wrapped in public api) */ getFiles, /** * Starts uploading all files */ processFiles, /** * Clears all files from the files list */ removeFiles, /** * Starts preparing output of all files */ prepareFiles, /** * Sort list of files */ sort: compare => store.dispatch('SORT', { compare }), /** * Browse the file system for a file */ browse: () => { // needs to be trigger directly as user action needs to be traceable (is not traceable in requestAnimationFrame) var input = view.element.querySelector('input[type=file]'); if (input) { input.click(); } }, /** * Destroys the app */ destroy: () => { // request destruction exports.fire('destroy', view.element); // stop active processes (file uploads, fetches, stuff like that) // loop over items and depending on states call abort for ongoing processes store.dispatch('ABORT_ALL'); // destroy view view._destroy(); // stop listening to resize window.removeEventListener('resize', resizeHandler); // stop listening to the visiblitychange event document.removeEventListener('visibilitychange', visibilityHandler); // dispatch destroy store.dispatch('DID_DESTROY'); }, /** * Inserts the plugin before the target element */ insertBefore: element => insertBefore(view.element, element), /** * Inserts the plugin after the target element */ insertAfter: element => insertAfter(view.element, element), /** * Appends the plugin to the target element */ appendTo: element => element.appendChild(view.element), /** * Replaces an element with the app */ replaceElement: element => { // insert the app before the element insertBefore(view.element, element); // remove the original element element.parentNode.removeChild(element); // remember original element originalElement = element; }, /** * Restores the original element */ restoreElement: () => { if (!originalElement) { return; // no element to restore } // restore original element insertAfter(originalElement, view.element); // remove our element view.element.parentNode.removeChild(view.element); // remove reference originalElement = null; }, /** * Returns true if the app root is attached to given element * @param element */ isAttachedTo: element => view.element === element || originalElement === element, /** * Returns the root element */ element: { get: () => view.element, }, /** * Returns the current pond status */ status: { get: () => store.query('GET_STATUS'), }, }; // Done! store.dispatch('DID_INIT'); // create actual api object return createObject(exports); }; const createAppObject = (customOptions = {}) => { // default options const defaultOptions = {}; forin(getOptions(), (key, value) => { defaultOptions[key] = value[0]; }); // set app options const app = createApp({ // default options ...defaultOptions, // custom options ...customOptions, }); // return the plugin instance return app; }; const lowerCaseFirstLetter = string => string.charAt(0).toLowerCase() + string.slice(1); const attributeNameToPropertyName = attributeName => toCamels(attributeName.replace(/^data-/, '')); const mapObject = (object, propertyMap) => { // remove unwanted forin(propertyMap, (selector, mapping) => { forin(object, (property, value) => { // create regexp shortcut const selectorRegExp = new RegExp(selector); // tests if const matches = selectorRegExp.test(property); // no match, skip if (!matches) { return; } // if there's a mapping, the original property is always removed delete object[property]; // should only remove, we done! if (mapping === false) { return; } // move value to new property if (isString(mapping)) { object[mapping] = value; return; } // move to group const group = mapping.group; if (isObject(mapping) && !object[group]) { object[group] = {}; } object[group][lowerCaseFirstLetter(property.replace(selectorRegExp, ''))] = value; }); // do submapping if (mapping.mapping) { mapObject(object[mapping.group], mapping.mapping); } }); }; const getAttributesAsObject = (node, attributeMapping = {}) => { // turn attributes into object const attributes = []; forin(node.attributes, index => { attributes.push(node.attributes[index]); }); const output = attributes .filter(attribute => attribute.name) .reduce((obj, attribute) => { const value = attr(node, attribute.name); obj[attributeNameToPropertyName(attribute.name)] = value === attribute.name ? true : value; return obj; }, {}); // do mapping of object properties mapObject(output, attributeMapping); return output; }; const createAppAtElement = (element, options = {}) => { // how attributes of the input element are mapped to the options for the plugin const attributeMapping = { // translate to other name '^class$': 'className', '^multiple$': 'allowMultiple', '^capture$': 'captureMethod', '^webkitdirectory$': 'allowDirectoriesOnly', // group under single property '^server': { group: 'server', mapping: { '^process': { group: 'process', }, '^revert': { group: 'revert', }, '^fetch': { group: 'fetch', }, '^restore': { group: 'restore', }, '^load': { group: 'load', }, }, }, // don't include in object '^type$': false, '^files$': false, }; // add additional option translators applyFilters('SET_ATTRIBUTE_TO_OPTION_MAP', attributeMapping); // create final options object by setting options object and then overriding options supplied on element const mergedOptions = { ...options, }; const attributeOptions = getAttributesAsObject( element.nodeName === 'FIELDSET' ? element.querySelector('input[type=file]') : element, attributeMapping ); // merge with options object Object.keys(attributeOptions).forEach(key => { if (isObject(attributeOptions[key])) { if (!isObject(mergedOptions[key])) { mergedOptions[key] = {}; } Object.assign(mergedOptions[key], attributeOptions[key]); } else { mergedOptions[key] = attributeOptions[key]; } }); // if parent is a fieldset, get files from parent by selecting all input fields that are not file upload fields // these will then be automatically set to the initial files mergedOptions.files = (options.files || []).concat( Array.from(element.querySelectorAll('input:not([type=file])')).map(input => ({ source: input.value, options: { type: input.dataset.type, }, })) ); // build plugin const app = createAppObject(mergedOptions); // add already selected files if (element.files) { Array.from(element.files).forEach(file => { app.addFile(file); }); } // replace the target element app.replaceElement(element); // expose return app; }; // if an element is passed, we create the instance at that element, if not, we just create an up object const createApp$1 = (...args) => isNode(args[0]) ? createAppAtElement(...args) : createAppObject(...args); const PRIVATE_METHODS = ['fire', '_read', '_write']; const createAppAPI = app => { const api = {}; copyObjectPropertiesToObject(app, api, PRIVATE_METHODS); return api; }; /** * Replaces placeholders in given string with replacements * @param string - "Foo {bar}"" * @param replacements - { "bar": 10 } */ const replaceInString = (string, replacements) => string.replace(/(?:{([a-zA-Z]+)})/g, (match, group) => replacements[group]); const createWorker = fn => { const workerBlob = new Blob(['(', fn.toString(), ')()'], { type: 'application/javascript', }); const workerURL = URL.createObjectURL(workerBlob); const worker = new Worker(workerURL); return { transfer: (message, cb) => {}, post: (message, cb, transferList) => { const id = getUniqueId(); worker.onmessage = e => { if (e.data.id === id) { cb(e.data.message); } }; worker.postMessage( { id, message, }, transferList ); }, terminate: () => { worker.terminate(); URL.revokeObjectURL(workerURL); }, }; }; const loadImage = url => new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { resolve(img); }; img.onerror = e => { reject(e); }; img.src = url; }); const renameFile = (file, name) => { const renamedFile = file.slice(0, file.size, file.type); renamedFile.lastModifiedDate = file.lastModifiedDate; renamedFile.name = name; return renamedFile; }; const copyFile = file => renameFile(file, file.name); // already registered plugins (can't register twice) const registeredPlugins = []; // pass utils to plugin const createAppPlugin = plugin => { // already registered if (registeredPlugins.includes(plugin)) { return; } // remember this plugin registeredPlugins.push(plugin); // setup! const pluginOutline = plugin({ addFilter, utils: { Type, forin, isString, isFile, toNaturalFileSize, replaceInString, getExtensionFromFilename, getFilenameWithoutExtension, guesstimateMimeType, getFileFromBlob, getFilenameFromURL, createRoute, createWorker, createView, createItemAPI, loadImage, copyFile, renameFile, createBlob, applyFilterChain, text, getNumericAspectRatioFromString, }, views: { fileActionButton, }, }); // add plugin options to default options extendDefaultOptions(pluginOutline.options); }; // feature detection used by supported() method const isOperaMini = () => Object.prototype.toString.call(window.operamini) === '[object OperaMini]'; const hasPromises = () => 'Promise' in window; const hasBlobSlice = () => 'slice' in Blob.prototype; const hasCreateObjectURL = () => 'URL' in window && 'createObjectURL' in window.URL; const hasVisibility = () => 'visibilityState' in document; const hasTiming = () => 'performance' in window; // iOS 8.x const hasCSSSupports = () => 'supports' in (window.CSS || {}); // use to detect Safari 9+ const isIE11 = () => /MSIE|Trident/.test(window.navigator.userAgent); const supported = (() => { // Runs immediately and then remembers result for subsequent calls const isSupported = // Has to be a browser isBrowser() && // Can't run on Opera Mini due to lack of everything !isOperaMini() && // Require these APIs to feature detect a modern browser hasVisibility() && hasPromises() && hasBlobSlice() && hasCreateObjectURL() && hasTiming() && // doesn't need CSSSupports but is a good way to detect Safari 9+ (we do want to support IE11 though) (hasCSSSupports() || isIE11()); return () => isSupported; })(); /** * Plugin internal state (over all instances) */ const state = { // active app instances, used to redraw the apps and to find the later apps: [], }; // plugin name const name = 'filepond'; /** * Public Plugin methods */ const fn = () => {}; let Status$1 = {}; let FileStatus = {}; let FileOrigin$1 = {}; let OptionTypes = {}; let create$f = fn; let destroy = fn; let parse = fn; let find = fn; let registerPlugin = fn; let getOptions$1 = fn; let setOptions$1 = fn; // if not supported, no API if (supported()) { // start painter and fire load event createPainter( () => { state.apps.forEach(app => app._read()); }, ts => { state.apps.forEach(app => app._write(ts)); } ); // fire loaded event so we know when FilePond is available const dispatch = () => { // let others know we have area ready document.dispatchEvent( new CustomEvent('FilePond:loaded', { detail: { supported, create: create$f, destroy, parse, find, registerPlugin, setOptions: setOptions$1, }, }) ); // clean up event document.removeEventListener('DOMContentLoaded', dispatch); }; if (document.readyState !== 'loading') { // move to back of execution queue, FilePond should have been exported by then setTimeout(() => dispatch(), 0); } else { document.addEventListener('DOMContentLoaded', dispatch); } // updates the OptionTypes object based on the current options const updateOptionTypes = () => forin(getOptions(), (key, value) => { OptionTypes[key] = value[1]; }); Status$1 = { ...Status }; FileOrigin$1 = { ...FileOrigin }; FileStatus = { ...ItemStatus }; OptionTypes = {}; updateOptionTypes(); // create method, creates apps and adds them to the app array create$f = (...args) => { const app = createApp$1(...args); app.on('destroy', destroy); state.apps.push(app); return createAppAPI(app); }; // destroys apps and removes them from the app array destroy = hook => { // returns true if the app was destroyed successfully const indexToRemove = state.apps.findIndex(app => app.isAttachedTo(hook)); if (indexToRemove >= 0) { // remove from apps const app = state.apps.splice(indexToRemove, 1)[0]; // restore original dom element app.restoreElement(); return true; } return false; }; // parses the given context for plugins (does not include the context element itself) parse = context => { // get all possible hooks const matchedHooks = Array.from(context.querySelectorAll(`.${name}`)); // filter out already active hooks const newHooks = matchedHooks.filter( newHook => !state.apps.find(app => app.isAttachedTo(newHook)) ); // create new instance for each hook return newHooks.map(hook => create$f(hook)); }; // returns an app based on the given element hook find = hook => { const app = state.apps.find(app => app.isAttachedTo(hook)); if (!app) { return null; } return createAppAPI(app); }; // adds a plugin extension registerPlugin = (...plugins) => { // register plugins plugins.forEach(createAppPlugin); // update OptionTypes, each plugin might have extended the default options updateOptionTypes(); }; getOptions$1 = () => { const opts = {}; forin(getOptions(), (key, value) => { opts[key] = value[0]; }); return opts; }; setOptions$1 = opts => { if (isObject(opts)) { // update existing plugins state.apps.forEach(app => { app.setOptions(opts); }); // override defaults setOptions(opts); } // return new options return getOptions$1(); }; } export { FileOrigin$1 as FileOrigin, FileStatus, OptionTypes, Status$1 as Status, create$f as create, destroy, find, getOptions$1 as getOptions, parse, registerPlugin, setOptions$1 as setOptions, supported, }; ================================================ FILE: dist/filepond.js ================================================ /*! * FilePond 4.32.12 * Licensed under MIT, https://opensource.org/licenses/MIT/ * Please visit https://pqina.nl/filepond/ for details. */ /* eslint-disable */ (function(global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : ((global = global || self), factory((global.FilePond = {}))); })(this, function(exports) { 'use strict'; var isNode = function isNode(value) { return value instanceof HTMLElement; }; var createStore = function createStore(initialState) { var queries = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var actions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; // internal state var state = Object.assign({}, initialState); // contains all actions for next frame, is clear when actions are requested var actionQueue = []; var dispatchQueue = []; // returns a duplicate of the current state var getState = function getState() { return Object.assign({}, state); }; // returns a duplicate of the actions array and clears the actions array var processActionQueue = function processActionQueue() { // create copy of actions queue var queue = [].concat(actionQueue); // clear actions queue (we don't want no double actions) actionQueue.length = 0; return queue; }; // processes actions that might block the main UI thread var processDispatchQueue = function processDispatchQueue() { // create copy of actions queue var queue = [].concat(dispatchQueue); // clear actions queue (we don't want no double actions) dispatchQueue.length = 0; // now dispatch these actions queue.forEach(function(_ref) { var type = _ref.type, data = _ref.data; dispatch(type, data); }); }; // adds a new action, calls its handler and var dispatch = function dispatch(type, data, isBlocking) { // is blocking action (should never block if document is hidden) if (isBlocking && !document.hidden) { dispatchQueue.push({ type: type, data: data }); return; } // if this action has a handler, handle the action if (actionHandlers[type]) { actionHandlers[type](data); } // now add action actionQueue.push({ type: type, data: data, }); }; var query = function query(str) { var _queryHandles; for ( var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++ ) { args[_key - 1] = arguments[_key]; } return queryHandles[str] ? (_queryHandles = queryHandles)[str].apply(_queryHandles, args) : null; }; var api = { getState: getState, processActionQueue: processActionQueue, processDispatchQueue: processDispatchQueue, dispatch: dispatch, query: query, }; var queryHandles = {}; queries.forEach(function(query) { queryHandles = Object.assign({}, query(state), {}, queryHandles); }); var actionHandlers = {}; actions.forEach(function(action) { actionHandlers = Object.assign({}, action(dispatch, query, state), {}, actionHandlers); }); return api; }; var defineProperty = function defineProperty(obj, property, definition) { if (typeof definition === 'function') { obj[property] = definition; return; } Object.defineProperty(obj, property, Object.assign({}, definition)); }; var forin = function forin(obj, cb) { for (var key in obj) { if (!obj.hasOwnProperty(key)) { continue; } cb(key, obj[key]); } }; var createObject = function createObject(definition) { var obj = {}; forin(definition, function(property) { defineProperty(obj, property, definition[property]); }); return obj; }; var attr = function attr(node, name) { var value = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; if (value === null) { return node.getAttribute(name) || node.hasAttribute(name); } node.setAttribute(name, value); }; var ns = 'http://www.w3.org/2000/svg'; var svgElements = ['svg', 'path']; // only svg elements used var isSVGElement = function isSVGElement(tag) { return svgElements.includes(tag); }; var createElement = function createElement(tag, className) { var attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; if (typeof className === 'object') { attributes = className; className = null; } var element = isSVGElement(tag) ? document.createElementNS(ns, tag) : document.createElement(tag); if (className) { if (isSVGElement(tag)) { attr(element, 'class', className); } else { element.className = className; } } forin(attributes, function(name, value) { attr(element, name, value); }); return element; }; var appendChild = function appendChild(parent) { return function(child, index) { if (typeof index !== 'undefined' && parent.children[index]) { parent.insertBefore(child, parent.children[index]); } else { parent.appendChild(child); } }; }; var appendChildView = function appendChildView(parent, childViews) { return function(view, index) { if (typeof index !== 'undefined') { childViews.splice(index, 0, view); } else { childViews.push(view); } return view; }; }; var removeChildView = function removeChildView(parent, childViews) { return function(view) { // remove from child views childViews.splice(childViews.indexOf(view), 1); // remove the element if (view.element.parentNode) { parent.removeChild(view.element); } return view; }; }; var IS_BROWSER = (function() { return typeof window !== 'undefined' && typeof window.document !== 'undefined'; })(); var isBrowser = function isBrowser() { return IS_BROWSER; }; var testElement = isBrowser() ? createElement('svg') : {}; var getChildCount = 'children' in testElement ? function(el) { return el.children.length; } : function(el) { return el.childNodes.length; }; var getViewRect = function getViewRect(elementRect, childViews, offset, scale) { var left = offset[0] || elementRect.left; var top = offset[1] || elementRect.top; var right = left + elementRect.width; var bottom = top + elementRect.height * (scale[1] || 1); var rect = { // the rectangle of the element itself element: Object.assign({}, elementRect), // the rectangle of the element expanded to contain its children, does not include any margins inner: { left: elementRect.left, top: elementRect.top, right: elementRect.right, bottom: elementRect.bottom, }, // the rectangle of the element expanded to contain its children including own margin and child margins // margins will be added after we've recalculated the size outer: { left: left, top: top, right: right, bottom: bottom, }, }; // expand rect to fit all child rectangles childViews .filter(function(childView) { return !childView.isRectIgnored(); }) .map(function(childView) { return childView.rect; }) .forEach(function(childViewRect) { expandRect(rect.inner, Object.assign({}, childViewRect.inner)); expandRect(rect.outer, Object.assign({}, childViewRect.outer)); }); // calculate inner width and height calculateRectSize(rect.inner); // append additional margin (top and left margins are included in top and left automatically) rect.outer.bottom += rect.element.marginBottom; rect.outer.right += rect.element.marginRight; // calculate outer width and height calculateRectSize(rect.outer); return rect; }; var expandRect = function expandRect(parent, child) { // adjust for parent offset child.top += parent.top; child.right += parent.left; child.bottom += parent.top; child.left += parent.left; if (child.bottom > parent.bottom) { parent.bottom = child.bottom; } if (child.right > parent.right) { parent.right = child.right; } }; var calculateRectSize = function calculateRectSize(rect) { rect.width = rect.right - rect.left; rect.height = rect.bottom - rect.top; }; var isNumber = function isNumber(value) { return typeof value === 'number'; }; /** * Determines if position is at destination * @param position * @param destination * @param velocity * @param errorMargin * @returns {boolean} */ var thereYet = function thereYet(position, destination, velocity) { var errorMargin = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0.001; return Math.abs(position - destination) < errorMargin && Math.abs(velocity) < errorMargin; }; /** * Spring animation */ var spring = // default options function spring() // method definition { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref$stiffness = _ref.stiffness, stiffness = _ref$stiffness === void 0 ? 0.5 : _ref$stiffness, _ref$damping = _ref.damping, damping = _ref$damping === void 0 ? 0.75 : _ref$damping, _ref$mass = _ref.mass, mass = _ref$mass === void 0 ? 10 : _ref$mass; var target = null; var position = null; var velocity = 0; var resting = false; // updates spring state var interpolate = function interpolate(ts, skipToEndState) { // in rest, don't animate if (resting) return; // need at least a target or position to do springy things if (!(isNumber(target) && isNumber(position))) { resting = true; velocity = 0; return; } // calculate spring force var f = -(position - target) * stiffness; // update velocity by adding force based on mass velocity += f / mass; // update position by adding velocity position += velocity; // slow down based on amount of damping velocity *= damping; // we've arrived if we're near target and our velocity is near zero if (thereYet(position, target, velocity) || skipToEndState) { position = target; velocity = 0; resting = true; // we done api.onupdate(position); api.oncomplete(position); } else { // progress update api.onupdate(position); } }; /** * Set new target value * @param value */ var setTarget = function setTarget(value) { // if currently has no position, set target and position to this value if (isNumber(value) && !isNumber(position)) { position = value; } // next target value will not be animated to if (target === null) { target = value; position = value; } // let start moving to target target = value; // already at target if (position === target || typeof target === 'undefined') { // now resting as target is current position, stop moving resting = true; velocity = 0; // done! api.onupdate(position); api.oncomplete(position); return; } resting = false; }; // need 'api' to call onupdate callback var api = createObject({ interpolate: interpolate, target: { set: setTarget, get: function get() { return target; }, }, resting: { get: function get() { return resting; }, }, onupdate: function onupdate(value) {}, oncomplete: function oncomplete(value) {}, }); return api; }; var easeLinear = function easeLinear(t) { return t; }; var easeInOutQuad = function easeInOutQuad(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }; var tween = // default values function tween() // method definition { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref$duration = _ref.duration, duration = _ref$duration === void 0 ? 500 : _ref$duration, _ref$easing = _ref.easing, easing = _ref$easing === void 0 ? easeInOutQuad : _ref$easing, _ref$delay = _ref.delay, delay = _ref$delay === void 0 ? 0 : _ref$delay; var start = null; var t; var p; var resting = true; var reverse = false; var target = null; var interpolate = function interpolate(ts, skipToEndState) { if (resting || target === null) return; if (start === null) { start = ts; } if (ts - start < delay) return; t = ts - start - delay; if (t >= duration || skipToEndState) { t = 1; p = reverse ? 0 : 1; api.onupdate(p * target); api.oncomplete(p * target); resting = true; } else { p = t / duration; api.onupdate((t >= 0 ? easing(reverse ? 1 - p : p) : 0) * target); } }; // need 'api' to call onupdate callback var api = createObject({ interpolate: interpolate, target: { get: function get() { return reverse ? 0 : target; }, set: function set(value) { // is initial value if (target === null) { target = value; api.onupdate(value); api.oncomplete(value); return; } // want to tween to a smaller value and have a current value if (value < target) { target = 1; reverse = true; } else { // not tweening to a smaller value reverse = false; target = value; } // let's go! resting = false; start = null; }, }, resting: { get: function get() { return resting; }, }, onupdate: function onupdate(value) {}, oncomplete: function oncomplete(value) {}, }); return api; }; var animator = { spring: spring, tween: tween, }; /* { type: 'spring', stiffness: .5, damping: .75, mass: 10 }; { translation: { type: 'spring', ... }, ... } { translation: { x: { type: 'spring', ... } } } */ var createAnimator = function createAnimator(definition, category, property) { // default is single definition // we check if transform is set, if so, we check if property is set var def = definition[category] && typeof definition[category][property] === 'object' ? definition[category][property] : definition[category] || definition; var type = typeof def === 'string' ? def : def.type; var props = typeof def === 'object' ? Object.assign({}, def) : {}; return animator[type] ? animator[type](props) : null; }; var addGetSet = function addGetSet(keys, obj, props) { var overwrite = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; obj = Array.isArray(obj) ? obj : [obj]; obj.forEach(function(o) { keys.forEach(function(key) { var name = key; var getter = function getter() { return props[key]; }; var setter = function setter(value) { return (props[key] = value); }; if (typeof key === 'object') { name = key.key; getter = key.getter || getter; setter = key.setter || setter; } if (o[name] && !overwrite) { return; } o[name] = { get: getter, set: setter, }; }); }); }; // add to state, // add getters and setters to internal and external api (if not set) // setup animators var animations = function animations(_ref) { var mixinConfig = _ref.mixinConfig, viewProps = _ref.viewProps, viewInternalAPI = _ref.viewInternalAPI, viewExternalAPI = _ref.viewExternalAPI; // initial properties var initialProps = Object.assign({}, viewProps); // list of all active animations var animations = []; // setup animators forin(mixinConfig, function(property, animation) { var animator = createAnimator(animation); if (!animator) { return; } // when the animator updates, update the view state value animator.onupdate = function(value) { viewProps[property] = value; }; // set animator target animator.target = initialProps[property]; // when value is set, set the animator target value var prop = { key: property, setter: function setter(value) { // if already at target, we done! if (animator.target === value) { return; } animator.target = value; }, getter: function getter() { return viewProps[property]; }, }; // add getters and setters addGetSet([prop], [viewInternalAPI, viewExternalAPI], viewProps, true); // add it to the list for easy updating from the _write method animations.push(animator); }); // expose internal write api return { write: function write(ts) { var skipToEndState = document.hidden; var resting = true; animations.forEach(function(animation) { if (!animation.resting) resting = false; animation.interpolate(ts, skipToEndState); }); return resting; }, destroy: function destroy() {}, }; }; var addEvent = function addEvent(element) { return function(type, fn) { element.addEventListener(type, fn); }; }; var removeEvent = function removeEvent(element) { return function(type, fn) { element.removeEventListener(type, fn); }; }; // mixin var listeners = function listeners(_ref) { var mixinConfig = _ref.mixinConfig, viewProps = _ref.viewProps, viewInternalAPI = _ref.viewInternalAPI, viewExternalAPI = _ref.viewExternalAPI, viewState = _ref.viewState, view = _ref.view; var events = []; var add = addEvent(view.element); var remove = removeEvent(view.element); viewExternalAPI.on = function(type, fn) { events.push({ type: type, fn: fn, }); add(type, fn); }; viewExternalAPI.off = function(type, fn) { events.splice( events.findIndex(function(event) { return event.type === type && event.fn === fn; }), 1 ); remove(type, fn); }; return { write: function write() { // not busy return true; }, destroy: function destroy() { events.forEach(function(event) { remove(event.type, event.fn); }); }, }; }; // add to external api and link to props var apis = function apis(_ref) { var mixinConfig = _ref.mixinConfig, viewProps = _ref.viewProps, viewExternalAPI = _ref.viewExternalAPI; addGetSet(mixinConfig, viewExternalAPI, viewProps); }; var isDefined = function isDefined(value) { return value != null; }; // add to state, // add getters and setters to internal and external api (if not set) // set initial state based on props in viewProps // apply as transforms each frame var defaults = { opacity: 1, scaleX: 1, scaleY: 1, translateX: 0, translateY: 0, rotateX: 0, rotateY: 0, rotateZ: 0, originX: 0, originY: 0, }; var styles = function styles(_ref) { var mixinConfig = _ref.mixinConfig, viewProps = _ref.viewProps, viewInternalAPI = _ref.viewInternalAPI, viewExternalAPI = _ref.viewExternalAPI, view = _ref.view; // initial props var initialProps = Object.assign({}, viewProps); // current props var currentProps = {}; // we will add those properties to the external API and link them to the viewState addGetSet(mixinConfig, [viewInternalAPI, viewExternalAPI], viewProps); // override rect on internal and external rect getter so it takes in account transforms var getOffset = function getOffset() { return [viewProps['translateX'] || 0, viewProps['translateY'] || 0]; }; var getScale = function getScale() { return [viewProps['scaleX'] || 0, viewProps['scaleY'] || 0]; }; var getRect = function getRect() { return view.rect ? getViewRect(view.rect, view.childViews, getOffset(), getScale()) : null; }; viewInternalAPI.rect = { get: getRect }; viewExternalAPI.rect = { get: getRect }; // apply view props mixinConfig.forEach(function(key) { viewProps[key] = typeof initialProps[key] === 'undefined' ? defaults[key] : initialProps[key]; }); // expose api return { write: function write() { // see if props have changed if (!propsHaveChanged(currentProps, viewProps)) { return; } // moves element to correct position on screen applyStyles(view.element, viewProps); // store new transforms Object.assign(currentProps, Object.assign({}, viewProps)); // no longer busy return true; }, destroy: function destroy() {}, }; }; var propsHaveChanged = function propsHaveChanged(currentProps, newProps) { // different amount of keys if (Object.keys(currentProps).length !== Object.keys(newProps).length) { return true; } // lets analyze the individual props for (var prop in newProps) { if (newProps[prop] !== currentProps[prop]) { return true; } } return false; }; var applyStyles = function applyStyles(element, _ref2) { var opacity = _ref2.opacity, perspective = _ref2.perspective, translateX = _ref2.translateX, translateY = _ref2.translateY, scaleX = _ref2.scaleX, scaleY = _ref2.scaleY, rotateX = _ref2.rotateX, rotateY = _ref2.rotateY, rotateZ = _ref2.rotateZ, originX = _ref2.originX, originY = _ref2.originY, width = _ref2.width, height = _ref2.height; var transforms = ''; var styles = ''; // handle transform origin if (isDefined(originX) || isDefined(originY)) { styles += 'transform-origin: ' + (originX || 0) + 'px ' + (originY || 0) + 'px;'; } // transform order is relevant // 0. perspective if (isDefined(perspective)) { transforms += 'perspective(' + perspective + 'px) '; } // 1. translate if (isDefined(translateX) || isDefined(translateY)) { transforms += 'translate3d(' + (translateX || 0) + 'px, ' + (translateY || 0) + 'px, 0) '; } // 2. scale if (isDefined(scaleX) || isDefined(scaleY)) { transforms += 'scale3d(' + (isDefined(scaleX) ? scaleX : 1) + ', ' + (isDefined(scaleY) ? scaleY : 1) + ', 1) '; } // 3. rotate if (isDefined(rotateZ)) { transforms += 'rotateZ(' + rotateZ + 'rad) '; } if (isDefined(rotateX)) { transforms += 'rotateX(' + rotateX + 'rad) '; } if (isDefined(rotateY)) { transforms += 'rotateY(' + rotateY + 'rad) '; } // add transforms if (transforms.length) { styles += 'transform:' + transforms + ';'; } // add opacity if (isDefined(opacity)) { styles += 'opacity:' + opacity + ';'; // if we reach zero, we make the element inaccessible if (opacity === 0) { styles += 'visibility:hidden;'; } // if we're below 100% opacity this element can't be clicked if (opacity < 1) { styles += 'pointer-events:none;'; } } // add height if (isDefined(height)) { styles += 'height:' + height + 'px;'; } // add width if (isDefined(width)) { styles += 'width:' + width + 'px;'; } // apply styles var elementCurrentStyle = element.elementCurrentStyle || ''; // if new styles does not match current styles, lets update! if (styles.length !== elementCurrentStyle.length || styles !== elementCurrentStyle) { element.style.cssText = styles; // store current styles so we can compare them to new styles later on // _not_ getting the style value is faster element.elementCurrentStyle = styles; } }; var Mixins = { styles: styles, listeners: listeners, animations: animations, apis: apis, }; var updateRect = function updateRect() { var rect = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var element = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var style = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; if (!element.layoutCalculated) { rect.paddingTop = parseInt(style.paddingTop, 10) || 0; rect.marginTop = parseInt(style.marginTop, 10) || 0; rect.marginRight = parseInt(style.marginRight, 10) || 0; rect.marginBottom = parseInt(style.marginBottom, 10) || 0; rect.marginLeft = parseInt(style.marginLeft, 10) || 0; element.layoutCalculated = true; } rect.left = element.offsetLeft || 0; rect.top = element.offsetTop || 0; rect.width = element.offsetWidth || 0; rect.height = element.offsetHeight || 0; rect.right = rect.left + rect.width; rect.bottom = rect.top + rect.height; rect.scrollTop = element.scrollTop; rect.hidden = element.offsetParent === null; return rect; }; var createView = // default view definition function createView() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref$tag = _ref.tag, tag = _ref$tag === void 0 ? 'div' : _ref$tag, _ref$name = _ref.name, name = _ref$name === void 0 ? null : _ref$name, _ref$attributes = _ref.attributes, attributes = _ref$attributes === void 0 ? {} : _ref$attributes, _ref$read = _ref.read, read = _ref$read === void 0 ? function() {} : _ref$read, _ref$write = _ref.write, write = _ref$write === void 0 ? function() {} : _ref$write, _ref$create = _ref.create, create = _ref$create === void 0 ? function() {} : _ref$create, _ref$destroy = _ref.destroy, destroy = _ref$destroy === void 0 ? function() {} : _ref$destroy, _ref$filterFrameActio = _ref.filterFrameActionsForChild, filterFrameActionsForChild = _ref$filterFrameActio === void 0 ? function(child, actions) { return actions; } : _ref$filterFrameActio, _ref$didCreateView = _ref.didCreateView, didCreateView = _ref$didCreateView === void 0 ? function() {} : _ref$didCreateView, _ref$didWriteView = _ref.didWriteView, didWriteView = _ref$didWriteView === void 0 ? function() {} : _ref$didWriteView, _ref$ignoreRect = _ref.ignoreRect, ignoreRect = _ref$ignoreRect === void 0 ? false : _ref$ignoreRect, _ref$ignoreRectUpdate = _ref.ignoreRectUpdate, ignoreRectUpdate = _ref$ignoreRectUpdate === void 0 ? false : _ref$ignoreRectUpdate, _ref$mixins = _ref.mixins, mixins = _ref$mixins === void 0 ? [] : _ref$mixins; return function( // each view requires reference to store store ) { var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; // root element should not be changed var element = createElement(tag, 'filepond--' + name, attributes); // style reference should also not be changed var style = window.getComputedStyle(element, null); // element rectangle var rect = updateRect(); var frameRect = null; // rest state var isResting = false; // pretty self explanatory var childViews = []; // loaded mixins var activeMixins = []; // references to created children var ref = {}; // state used for each instance var state = {}; // list of writers that will be called to update this view var writers = [ write, // default writer ]; var readers = [ read, // default reader ]; var destroyers = [ destroy, // default destroy ]; // core view methods var getElement = function getElement() { return element; }; var getChildViews = function getChildViews() { return childViews.concat(); }; var getReference = function getReference() { return ref; }; var createChildView = function createChildView(store) { return function(view, props) { return view(store, props); }; }; var getRect = function getRect() { if (frameRect) { return frameRect; } frameRect = getViewRect(rect, childViews, [0, 0], [1, 1]); return frameRect; }; var getStyle = function getStyle() { return style; }; /** * Read data from DOM * @private */ var _read = function _read() { frameRect = null; // read child views childViews.forEach(function(child) { return child._read(); }); var shouldUpdate = !(ignoreRectUpdate && rect.width && rect.height); if (shouldUpdate) { updateRect(rect, element, style); } // readers var api = { root: internalAPI, props: props, rect: rect }; readers.forEach(function(reader) { return reader(api); }); }; /** * Write data to DOM * @private */ var _write = function _write(ts, frameActions, shouldOptimize) { // if no actions, we assume that the view is resting var resting = frameActions.length === 0; // writers writers.forEach(function(writer) { var writerResting = writer({ props: props, root: internalAPI, actions: frameActions, timestamp: ts, shouldOptimize: shouldOptimize, }); if (writerResting === false) { resting = false; } }); // run mixins activeMixins.forEach(function(mixin) { // if one of the mixins is still busy after write operation, we are not resting var mixinResting = mixin.write(ts); if (mixinResting === false) { resting = false; } }); // updates child views that are currently attached to the DOM childViews .filter(function(child) { return !!child.element.parentNode; }) .forEach(function(child) { // if a child view is not resting, we are not resting var childResting = child._write( ts, filterFrameActionsForChild(child, frameActions), shouldOptimize ); if (!childResting) { resting = false; } }); // append new elements to DOM and update those childViews //.filter(child => !child.element.parentNode) .forEach(function(child, index) { // skip if (child.element.parentNode) { return; } // append to DOM internalAPI.appendChild(child.element, index); // call read (need to know the size of these elements) child._read(); // re-call write child._write( ts, filterFrameActionsForChild(child, frameActions), shouldOptimize ); // we just added somthing to the dom, no rest resting = false; }); // update resting state isResting = resting; didWriteView({ props: props, root: internalAPI, actions: frameActions, timestamp: ts, }); // let parent know if we are resting return resting; }; var _destroy = function _destroy() { activeMixins.forEach(function(mixin) { return mixin.destroy(); }); destroyers.forEach(function(destroyer) { destroyer({ root: internalAPI, props: props }); }); childViews.forEach(function(child) { return child._destroy(); }); }; // sharedAPI var sharedAPIDefinition = { element: { get: getElement, }, style: { get: getStyle, }, childViews: { get: getChildViews, }, }; // private API definition var internalAPIDefinition = Object.assign({}, sharedAPIDefinition, { rect: { get: getRect, }, // access to custom children references ref: { get: getReference, }, // dom modifiers is: function is(needle) { return name === needle; }, appendChild: appendChild(element), createChildView: createChildView(store), linkView: function linkView(view) { childViews.push(view); return view; }, unlinkView: function unlinkView(view) { childViews.splice(childViews.indexOf(view), 1); }, appendChildView: appendChildView(element, childViews), removeChildView: removeChildView(element, childViews), registerWriter: function registerWriter(writer) { return writers.push(writer); }, registerReader: function registerReader(reader) { return readers.push(reader); }, registerDestroyer: function registerDestroyer(destroyer) { return destroyers.push(destroyer); }, invalidateLayout: function invalidateLayout() { return (element.layoutCalculated = false); }, // access to data store dispatch: store.dispatch, query: store.query, }); // public view API methods var externalAPIDefinition = { element: { get: getElement, }, childViews: { get: getChildViews, }, rect: { get: getRect, }, resting: { get: function get() { return isResting; }, }, isRectIgnored: function isRectIgnored() { return ignoreRect; }, _read: _read, _write: _write, _destroy: _destroy, }; // mixin API methods var mixinAPIDefinition = Object.assign({}, sharedAPIDefinition, { rect: { get: function get() { return rect; }, }, }); // add mixin functionality Object.keys(mixins) .sort(function(a, b) { // move styles to the back of the mixin list (so adjustments of other mixins are applied to the props correctly) if (a === 'styles') { return 1; } else if (b === 'styles') { return -1; } return 0; }) .forEach(function(key) { var mixinAPI = Mixins[key]({ mixinConfig: mixins[key], viewProps: props, viewState: state, viewInternalAPI: internalAPIDefinition, viewExternalAPI: externalAPIDefinition, view: createObject(mixinAPIDefinition), }); if (mixinAPI) { activeMixins.push(mixinAPI); } }); // construct private api var internalAPI = createObject(internalAPIDefinition); // create the view create({ root: internalAPI, props: props, }); // append created child views to root node var childCount = getChildCount(element); // need to know the current child count so appending happens in correct order childViews.forEach(function(child, index) { internalAPI.appendChild(child.element, childCount + index); }); // call did create didCreateView(internalAPI); // expose public api return createObject(externalAPIDefinition); }; }; var createPainter = function createPainter(read, write) { var fps = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 60; var name = '__framePainter'; // set global painter if (window[name]) { window[name].readers.push(read); window[name].writers.push(write); return; } window[name] = { readers: [read], writers: [write], }; var painter = window[name]; var interval = 1000 / fps; var last = null; var id = null; var requestTick = null; var cancelTick = null; var setTimerType = function setTimerType() { if (document.hidden) { requestTick = function requestTick() { return window.setTimeout(function() { return tick(performance.now()); }, interval); }; cancelTick = function cancelTick() { return window.clearTimeout(id); }; } else { requestTick = function requestTick() { return window.requestAnimationFrame(tick); }; cancelTick = function cancelTick() { return window.cancelAnimationFrame(id); }; } }; document.addEventListener('visibilitychange', function() { if (cancelTick) cancelTick(); setTimerType(); tick(performance.now()); }); var tick = function tick(ts) { // queue next tick id = requestTick(tick); // limit fps if (!last) { last = ts; } var delta = ts - last; if (delta <= interval) { // skip frame return; } // align next frame last = ts - (delta % interval); // update view painter.readers.forEach(function(read) { return read(); }); painter.writers.forEach(function(write) { return write(ts); }); }; setTimerType(); tick(performance.now()); return { pause: function pause() { cancelTick(id); }, }; }; var createRoute = function createRoute(routes, fn) { return function(_ref) { var root = _ref.root, props = _ref.props, _ref$actions = _ref.actions, actions = _ref$actions === void 0 ? [] : _ref$actions, timestamp = _ref.timestamp, shouldOptimize = _ref.shouldOptimize; actions .filter(function(action) { return routes[action.type]; }) .forEach(function(action) { return routes[action.type]({ root: root, props: props, action: action.data, timestamp: timestamp, shouldOptimize: shouldOptimize, }); }); if (fn) { fn({ root: root, props: props, actions: actions, timestamp: timestamp, shouldOptimize: shouldOptimize, }); } }; }; var insertBefore = function insertBefore(newNode, referenceNode) { return referenceNode.parentNode.insertBefore(newNode, referenceNode); }; var insertAfter = function insertAfter(newNode, referenceNode) { return referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); }; var isArray = function isArray(value) { return Array.isArray(value); }; var isEmpty = function isEmpty(value) { return value == null; }; var trim = function trim(str) { return str.trim(); }; var toString = function toString(value) { return '' + value; }; var toArray = function toArray(value) { var splitter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ','; if (isEmpty(value)) { return []; } if (isArray(value)) { return value; } return toString(value) .split(splitter) .map(trim) .filter(function(str) { return str.length; }); }; var isBoolean = function isBoolean(value) { return typeof value === 'boolean'; }; var toBoolean = function toBoolean(value) { return isBoolean(value) ? value : value === 'true'; }; var isString = function isString(value) { return typeof value === 'string'; }; var toNumber = function toNumber(value) { return isNumber(value) ? value : isString(value) ? toString(value).replace(/[a-z]+/gi, '') : 0; }; var toInt = function toInt(value) { return parseInt(toNumber(value), 10); }; var toFloat = function toFloat(value) { return parseFloat(toNumber(value)); }; var isInt = function isInt(value) { return isNumber(value) && isFinite(value) && Math.floor(value) === value; }; var toBytes = function toBytes(value) { var base = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1000; // is in bytes if (isInt(value)) { return value; } // is natural file size var naturalFileSize = toString(value).trim(); // if is value in megabytes if (/MB$/i.test(naturalFileSize)) { naturalFileSize = naturalFileSize.replace(/MB$i/, '').trim(); return toInt(naturalFileSize) * base * base; } // if is value in kilobytes if (/KB/i.test(naturalFileSize)) { naturalFileSize = naturalFileSize.replace(/KB$i/, '').trim(); return toInt(naturalFileSize) * base; } return toInt(naturalFileSize); }; var isFunction = function isFunction(value) { return typeof value === 'function'; }; var toFunctionReference = function toFunctionReference(string) { var ref = self; var levels = string.split('.'); var level = null; while ((level = levels.shift())) { ref = ref[level]; if (!ref) { return null; } } return ref; }; var methods = { process: 'POST', patch: 'PATCH', revert: 'DELETE', fetch: 'GET', restore: 'GET', load: 'GET', }; var createServerAPI = function createServerAPI(outline) { var api = {}; api.url = isString(outline) ? outline : outline.url || ''; api.timeout = outline.timeout ? parseInt(outline.timeout, 10) : 0; api.headers = outline.headers ? outline.headers : {}; forin(methods, function(key) { api[key] = createAction(key, outline[key], methods[key], api.timeout, api.headers); }); // remove process if no url or process on outline api.process = outline.process || isString(outline) || outline.url ? api.process : null; // special treatment for remove api.remove = outline.remove || null; // remove generic headers from api object delete api.headers; return api; }; var createAction = function createAction(name, outline, method, timeout, headers) { // is explicitely set to null so disable if (outline === null) { return null; } // if is custom function, done! Dev handles everything. if (typeof outline === 'function') { return outline; } // build action object var action = { url: method === 'GET' || method === 'PATCH' ? '?' + name + '=' : '', method: method, headers: headers, withCredentials: false, timeout: timeout, onload: null, ondata: null, onerror: null, }; // is a single url if (isString(outline)) { action.url = outline; return action; } // overwrite Object.assign(action, outline); // see if should reformat headers; if (isString(action.headers)) { var parts = action.headers.split(/:(.+)/); action.headers = { header: parts[0], value: parts[1], }; } // if is bool withCredentials action.withCredentials = toBoolean(action.withCredentials); return action; }; var toServerAPI = function toServerAPI(value) { return createServerAPI(value); }; var isNull = function isNull(value) { return value === null; }; var isObject = function isObject(value) { return typeof value === 'object' && value !== null; }; var isAPI = function isAPI(value) { return ( isObject(value) && isString(value.url) && isObject(value.process) && isObject(value.revert) && isObject(value.restore) && isObject(value.fetch) ); }; var getType = function getType(value) { if (isArray(value)) { return 'array'; } if (isNull(value)) { return 'null'; } if (isInt(value)) { return 'int'; } if (/^[0-9]+ ?(?:GB|MB|KB)$/gi.test(value)) { return 'bytes'; } if (isAPI(value)) { return 'api'; } return typeof value; }; var replaceSingleQuotes = function replaceSingleQuotes(str) { return str .replace(/{\s*'/g, '{"') .replace(/'\s*}/g, '"}') .replace(/'\s*:/g, '":') .replace(/:\s*'/g, ':"') .replace(/,\s*'/g, ',"') .replace(/'\s*,/g, '",'); }; var conversionTable = { array: toArray, boolean: toBoolean, int: function int(value) { return getType(value) === 'bytes' ? toBytes(value) : toInt(value); }, number: toFloat, float: toFloat, bytes: toBytes, string: function string(value) { return isFunction(value) ? value : toString(value); }, function: function _function(value) { return toFunctionReference(value); }, serverapi: toServerAPI, object: function object(value) { try { return JSON.parse(replaceSingleQuotes(value)); } catch (e) { return null; } }, }; var convertTo = function convertTo(value, type) { return conversionTable[type](value); }; var getValueByType = function getValueByType(newValue, defaultValue, valueType) { // can always assign default value if (newValue === defaultValue) { return newValue; } // get the type of the new value var newValueType = getType(newValue); // is valid type? if (newValueType !== valueType) { // is string input, let's attempt to convert var convertedValue = convertTo(newValue, valueType); // what is the type now newValueType = getType(convertedValue); // no valid conversions found if (convertedValue === null) { throw 'Trying to assign value with incorrect type to "' + option + '", allowed type: "' + valueType + '"'; } else { newValue = convertedValue; } } // assign new value return newValue; }; var createOption = function createOption(defaultValue, valueType) { var currentValue = defaultValue; return { enumerable: true, get: function get() { return currentValue; }, set: function set(newValue) { currentValue = getValueByType(newValue, defaultValue, valueType); }, }; }; var createOptions = function createOptions(options) { var obj = {}; forin(options, function(prop) { var optionDefinition = options[prop]; obj[prop] = createOption(optionDefinition[0], optionDefinition[1]); }); return createObject(obj); }; var createInitialState = function createInitialState(options) { return { // model items: [], // timeout used for calling update items listUpdateTimeout: null, // timeout used for stacking metadata updates itemUpdateTimeout: null, // queue of items waiting to be processed processingQueue: [], // options options: createOptions(options), }; }; var fromCamels = function fromCamels(string) { var separator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '-'; return string .split(/(?=[A-Z])/) .map(function(part) { return part.toLowerCase(); }) .join(separator); }; var createOptionAPI = function createOptionAPI(store, options) { var obj = {}; forin(options, function(key) { obj[key] = { get: function get() { return store.getState().options[key]; }, set: function set(value) { store.dispatch('SET_' + fromCamels(key, '_').toUpperCase(), { value: value, }); }, }; }); return obj; }; var createOptionActions = function createOptionActions(options) { return function(dispatch, query, state) { var obj = {}; forin(options, function(key) { var name = fromCamels(key, '_').toUpperCase(); obj['SET_' + name] = function(action) { try { state.options[key] = action.value; } catch (e) {} // nope, failed // we successfully set the value of this option dispatch('DID_SET_' + name, { value: state.options[key] }); }; }); return obj; }; }; var createOptionQueries = function createOptionQueries(options) { return function(state) { var obj = {}; forin(options, function(key) { obj['GET_' + fromCamels(key, '_').toUpperCase()] = function(action) { return state.options[key]; }; }); return obj; }; }; var InteractionMethod = { API: 1, DROP: 2, BROWSE: 3, PASTE: 4, NONE: 5, }; var getUniqueId = function getUniqueId() { return Math.random() .toString(36) .substring(2, 11); }; function _typeof(obj) { if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') { _typeof = function(obj) { return typeof obj; }; } else { _typeof = function(obj) { return obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype ? 'symbol' : typeof obj; }; } return _typeof(obj); } var REACT_ELEMENT_TYPE; function _jsx(type, props, key, children) { if (!REACT_ELEMENT_TYPE) { REACT_ELEMENT_TYPE = (typeof Symbol === 'function' && Symbol['for'] && Symbol['for']('react.element')) || 0xeac7; } var defaultProps = type && type.defaultProps; var childrenLength = arguments.length - 3; if (!props && childrenLength !== 0) { props = { children: void 0, }; } if (props && defaultProps) { for (var propName in defaultProps) { if (props[propName] === void 0) { props[propName] = defaultProps[propName]; } } } else if (!props) { props = defaultProps || {}; } if (childrenLength === 1) { props.children = children; } else if (childrenLength > 1) { var childArray = new Array(childrenLength); for (var i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 3]; } props.children = childArray; } return { $$typeof: REACT_ELEMENT_TYPE, type: type, key: key === undefined ? null : '' + key, ref: null, props: props, _owner: null, }; } function _asyncIterator(iterable) { var method; if (typeof Symbol !== 'undefined') { if (Symbol.asyncIterator) { method = iterable[Symbol.asyncIterator]; if (method != null) return method.call(iterable); } if (Symbol.iterator) { method = iterable[Symbol.iterator]; if (method != null) return method.call(iterable); } } throw new TypeError('Object is not async iterable'); } function _AwaitValue(value) { this.wrapped = value; } function _AsyncGenerator(gen) { var front, back; function send(key, arg) { return new Promise(function(resolve, reject) { var request = { key: key, arg: arg, resolve: resolve, reject: reject, next: null, }; if (back) { back = back.next = request; } else { front = back = request; resume(key, arg); } }); } function resume(key, arg) { try { var result = gen[key](arg); var value = result.value; var wrappedAwait = value instanceof _AwaitValue; Promise.resolve(wrappedAwait ? value.wrapped : value).then( function(arg) { if (wrappedAwait) { resume('next', arg); return; } settle(result.done ? 'return' : 'normal', arg); }, function(err) { resume('throw', err); } ); } catch (err) { settle('throw', err); } } function settle(type, value) { switch (type) { case 'return': front.resolve({ value: value, done: true, }); break; case 'throw': front.reject(value); break; default: front.resolve({ value: value, done: false, }); break; } front = front.next; if (front) { resume(front.key, front.arg); } else { back = null; } } this._invoke = send; if (typeof gen.return !== 'function') { this.return = undefined; } } if (typeof Symbol === 'function' && Symbol.asyncIterator) { _AsyncGenerator.prototype[Symbol.asyncIterator] = function() { return this; }; } _AsyncGenerator.prototype.next = function(arg) { return this._invoke('next', arg); }; _AsyncGenerator.prototype.throw = function(arg) { return this._invoke('throw', arg); }; _AsyncGenerator.prototype.return = function(arg) { return this._invoke('return', arg); }; function _wrapAsyncGenerator(fn) { return function() { return new _AsyncGenerator(fn.apply(this, arguments)); }; } function _awaitAsyncGenerator(value) { return new _AwaitValue(value); } function _asyncGeneratorDelegate(inner, awaitWrap) { var iter = {}, waiting = false; function pump(key, value) { waiting = true; value = new Promise(function(resolve) { resolve(inner[key](value)); }); return { done: false, value: awaitWrap(value), }; } if (typeof Symbol === 'function' && Symbol.iterator) { iter[Symbol.iterator] = function() { return this; }; } iter.next = function(value) { if (waiting) { waiting = false; return value; } return pump('next', value); }; if (typeof inner.throw === 'function') { iter.throw = function(value) { if (waiting) { waiting = false; throw value; } return pump('throw', value); }; } if (typeof inner.return === 'function') { iter.return = function(value) { return pump('return', value); }; } return iter; } function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _asyncToGenerator(fn) { return function() { var self = this, args = arguments; return new Promise(function(resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err); } _next(undefined); }); }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _defineEnumerableProperties(obj, descs) { for (var key in descs) { var desc = descs[key]; desc.configurable = desc.enumerable = true; if ('value' in desc) desc.writable = true; Object.defineProperty(obj, key, desc); } if (Object.getOwnPropertySymbols) { var objectSymbols = Object.getOwnPropertySymbols(descs); for (var i = 0; i < objectSymbols.length; i++) { var sym = objectSymbols[i]; var desc = descs[sym]; desc.configurable = desc.enumerable = true; if ('value' in desc) desc.writable = true; Object.defineProperty(obj, sym, desc); } } return obj; } function _defaults(obj, defaults) { var keys = Object.getOwnPropertyNames(defaults); for (var i = 0; i < keys.length; i++) { var key = keys[i]; var value = Object.getOwnPropertyDescriptor(defaults, key); if (value && value.configurable && obj[key] === undefined) { Object.defineProperty(obj, key, value); } } return obj; } 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; } 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 _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat( Object.getOwnPropertySymbols(source).filter(function(sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; }) ); } ownKeys.forEach(function(key) { _defineProperty(target, key, source[key]); }); } return target; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function(sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function(key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function(key) { Object.defineProperty( target, key, Object.getOwnPropertyDescriptor(source, key) ); }); } } return target; } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function'); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true, }, }); if (superClass) _setPrototypeOf(subClass, superClass); } function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function isNativeReflectConstruct() { if (typeof Reflect === 'undefined' || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === 'function') return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function() {})); return true; } catch (e) { return false; } } function _construct(Parent, args, Class) { if (isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); } function _isNativeFunction(fn) { return Function.toString.call(fn).indexOf('[native code]') !== -1; } function _wrapNativeSuper(Class) { var _cache = typeof Map === 'function' ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== 'function') { throw new TypeError('Super expression must either be null or a function'); } if (typeof _cache !== 'undefined') { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true, }, }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); } function _instanceof(left, right) { if (right != null && typeof Symbol !== 'undefined' && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj, }; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } function _newArrowCheck(innerThis, boundThis) { if (innerThis !== boundThis) { throw new TypeError('Cannot instantiate an arrow function'); } } function _objectDestructuringEmpty(obj) { if (obj == null) throw new TypeError('Cannot destructure undefined'); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _possibleConstructorReturn(self, call) { if (call && (typeof call === 'object' || typeof call === 'function')) { return call; } return _assertThisInitialized(self); } function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; } function _get(target, property, receiver) { if (typeof Reflect !== 'undefined' && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); } function set(target, property, value, receiver) { if (typeof Reflect !== 'undefined' && Reflect.set) { set = Reflect.set; } else { set = function set(target, property, value, receiver) { var base = _superPropBase(target, property); var desc; if (base) { desc = Object.getOwnPropertyDescriptor(base, property); if (desc.set) { desc.set.call(receiver, value); return true; } else if (!desc.writable) { return false; } } desc = Object.getOwnPropertyDescriptor(receiver, property); if (desc) { if (!desc.writable) { return false; } desc.value = value; Object.defineProperty(receiver, property, desc); } else { _defineProperty(receiver, property, value); } return true; }; } return set(target, property, value, receiver); } function _set(target, property, value, receiver, isStrict) { var s = set(target, property, value, receiver || target); if (!s && isStrict) { throw new Error('failed to set property'); } return value; } function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze( Object.defineProperties(strings, { raw: { value: Object.freeze(raw), }, }) ); } function _taggedTemplateLiteralLoose(strings, raw) { if (!raw) { raw = strings.slice(0); } strings.raw = raw; return strings; } function _temporalRef(val, name) { if (val === _temporalUndefined) { throw new ReferenceError(name + ' is not defined - temporal dead zone'); } else { return val; } } function _readOnlyError(name) { throw new Error('"' + name + '" is read-only'); } function _classNameTDZError(name) { throw new Error('Class "' + name + '" cannot be referenced in computed property keys.'); } var _temporalUndefined = {}; function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } function _slicedToArrayLoose(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimitLoose(arr, i) || _nonIterableRest(); } function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _nonIterableRest(); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArray(iter) { if ( Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === '[object Arguments]' ) return Array.from(iter); } function _iterableToArrayLimit(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'] != null) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } function _iterableToArrayLimitLoose(arr, i) { var _arr = []; for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done; ) { _arr.push(_step.value); if (i && _arr.length === i) break; } return _arr; } function _nonIterableSpread() { throw new TypeError('Invalid attempt to spread non-iterable instance'); } function _nonIterableRest() { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } function _skipFirstGeneratorNext(fn) { return function() { var it = fn.apply(this, arguments); it.next(); return it; }; } function _toPrimitive(input, hint) { if (typeof input !== 'object' || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || 'default'); if (typeof res !== 'object') return res; throw new TypeError('@@toPrimitive must return a primitive value.'); } return (hint === 'string' ? String : Number)(input); } function _toPropertyKey(arg) { var key = _toPrimitive(arg, 'string'); return typeof key === 'symbol' ? key : String(key); } function _initializerWarningHelper(descriptor, context) { throw new Error( 'Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and set to use loose mode. ' + 'To use proposal-class-properties in spec mode with decorators, wait for ' + 'the next major version of decorators in stage 2.' ); } function _initializerDefineProperty(target, property, descriptor, context) { if (!descriptor) return; Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0, }); } function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object.keys(descriptor).forEach(function(key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators .slice() .reverse() .reduce(function(desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object.defineProperty(target, property, desc); desc = null; } return desc; } var id = 0; function _classPrivateFieldLooseKey(name) { return '__private_' + id++ + '_' + name; } function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError('attempted to use private field on non-instance'); } return receiver; } function _classPrivateFieldGet(receiver, privateMap) { var descriptor = privateMap.get(receiver); if (!descriptor) { throw new TypeError('attempted to get private field on non-instance'); } if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; } function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = privateMap.get(receiver); if (!descriptor) { throw new TypeError('attempted to set private field on non-instance'); } if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError('attempted to set read only private field'); } descriptor.value = value; } return value; } function _classPrivateFieldDestructureSet(receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError('attempted to set private field on non-instance'); } var descriptor = privateMap.get(receiver); if (descriptor.set) { if (!('__destrObj' in descriptor)) { descriptor.__destrObj = { set value(v) { descriptor.set.call(receiver, v); }, }; } return descriptor.__destrObj; } else { if (!descriptor.writable) { throw new TypeError('attempted to set read only private field'); } return descriptor; } } function _classStaticPrivateFieldSpecGet(receiver, classConstructor, descriptor) { if (receiver !== classConstructor) { throw new TypeError('Private static access of wrong provenance'); } return descriptor.value; } function _classStaticPrivateFieldSpecSet(receiver, classConstructor, descriptor, value) { if (receiver !== classConstructor) { throw new TypeError('Private static access of wrong provenance'); } if (!descriptor.writable) { throw new TypeError('attempted to set read only private field'); } descriptor.value = value; return value; } function _classStaticPrivateMethodGet(receiver, classConstructor, method) { if (receiver !== classConstructor) { throw new TypeError('Private static access of wrong provenance'); } return method; } function _classStaticPrivateMethodSet() { throw new TypeError('attempted to set read only static private field'); } function _decorate(decorators, factory, superClass, mixins) { var api = _getDecoratorsApi(); if (mixins) { for (var i = 0; i < mixins.length; i++) { api = mixins[i](api); } } var r = factory(function initialize(O) { api.initializeInstanceElements(O, decorated.elements); }, superClass); var decorated = api.decorateClass( _coalesceClassElements(r.d.map(_createElementDescriptor)), decorators ); api.initializeClassElements(r.F, decorated.elements); return api.runClassFinishers(r.F, decorated.finishers); } function _getDecoratorsApi() { _getDecoratorsApi = function() { return api; }; var api = { elementsDefinitionOrder: [['method'], ['field']], initializeInstanceElements: function(O, elements) { ['method', 'field'].forEach(function(kind) { elements.forEach(function(element) { if (element.kind === kind && element.placement === 'own') { this.defineClassElement(O, element); } }, this); }, this); }, initializeClassElements: function(F, elements) { var proto = F.prototype; ['method', 'field'].forEach(function(kind) { elements.forEach(function(element) { var placement = element.placement; if ( element.kind === kind && (placement === 'static' || placement === 'prototype') ) { var receiver = placement === 'static' ? F : proto; this.defineClassElement(receiver, element); } }, this); }, this); }, defineClassElement: function(receiver, element) { var descriptor = element.descriptor; if (element.kind === 'field') { var initializer = element.initializer; descriptor = { enumerable: descriptor.enumerable, writable: descriptor.writable, configurable: descriptor.configurable, value: initializer === void 0 ? void 0 : initializer.call(receiver), }; } Object.defineProperty(receiver, element.key, descriptor); }, decorateClass: function(elements, decorators) { var newElements = []; var finishers = []; var placements = { static: [], prototype: [], own: [], }; elements.forEach(function(element) { this.addElementPlacement(element, placements); }, this); elements.forEach(function(element) { if (!_hasDecorators(element)) return newElements.push(element); var elementFinishersExtras = this.decorateElement(element, placements); newElements.push(elementFinishersExtras.element); newElements.push.apply(newElements, elementFinishersExtras.extras); finishers.push.apply(finishers, elementFinishersExtras.finishers); }, this); if (!decorators) { return { elements: newElements, finishers: finishers, }; } var result = this.decorateConstructor(newElements, decorators); finishers.push.apply(finishers, result.finishers); result.finishers = finishers; return result; }, addElementPlacement: function(element, placements, silent) { var keys = placements[element.placement]; if (!silent && keys.indexOf(element.key) !== -1) { throw new TypeError('Duplicated element (' + element.key + ')'); } keys.push(element.key); }, decorateElement: function(element, placements) { var extras = []; var finishers = []; for (var decorators = element.decorators, i = decorators.length - 1; i >= 0; i--) { var keys = placements[element.placement]; keys.splice(keys.indexOf(element.key), 1); var elementObject = this.fromElementDescriptor(element); var elementFinisherExtras = this.toElementFinisherExtras( (0, decorators[i])(elementObject) || elementObject ); element = elementFinisherExtras.element; this.addElementPlacement(element, placements); if (elementFinisherExtras.finisher) { finishers.push(elementFinisherExtras.finisher); } var newExtras = elementFinisherExtras.extras; if (newExtras) { for (var j = 0; j < newExtras.length; j++) { this.addElementPlacement(newExtras[j], placements); } extras.push.apply(extras, newExtras); } } return { element: element, finishers: finishers, extras: extras, }; }, decorateConstructor: function(elements, decorators) { var finishers = []; for (var i = decorators.length - 1; i >= 0; i--) { var obj = this.fromClassDescriptor(elements); var elementsAndFinisher = this.toClassDescriptor( (0, decorators[i])(obj) || obj ); if (elementsAndFinisher.finisher !== undefined) { finishers.push(elementsAndFinisher.finisher); } if (elementsAndFinisher.elements !== undefined) { elements = elementsAndFinisher.elements; for (var j = 0; j < elements.length - 1; j++) { for (var k = j + 1; k < elements.length; k++) { if ( elements[j].key === elements[k].key && elements[j].placement === elements[k].placement ) { throw new TypeError( 'Duplicated element (' + elements[j].key + ')' ); } } } } } return { elements: elements, finishers: finishers, }; }, fromElementDescriptor: function(element) { var obj = { kind: element.kind, key: element.key, placement: element.placement, descriptor: element.descriptor, }; var desc = { value: 'Descriptor', configurable: true, }; Object.defineProperty(obj, Symbol.toStringTag, desc); if (element.kind === 'field') obj.initializer = element.initializer; return obj; }, toElementDescriptors: function(elementObjects) { if (elementObjects === undefined) return; return _toArray(elementObjects).map(function(elementObject) { var element = this.toElementDescriptor(elementObject); this.disallowProperty(elementObject, 'finisher', 'An element descriptor'); this.disallowProperty(elementObject, 'extras', 'An element descriptor'); return element; }, this); }, toElementDescriptor: function(elementObject) { var kind = String(elementObject.kind); if (kind !== 'method' && kind !== 'field') { throw new TypeError( 'An element descriptor\'s .kind property must be either "method" or' + ' "field", but a decorator created an element descriptor with' + ' .kind "' + kind + '"' ); } var key = _toPropertyKey(elementObject.key); var placement = String(elementObject.placement); if (placement !== 'static' && placement !== 'prototype' && placement !== 'own') { throw new TypeError( 'An element descriptor\'s .placement property must be one of "static",' + ' "prototype" or "own", but a decorator created an element descriptor' + ' with .placement "' + placement + '"' ); } var descriptor = elementObject.descriptor; this.disallowProperty(elementObject, 'elements', 'An element descriptor'); var element = { kind: kind, key: key, placement: placement, descriptor: Object.assign({}, descriptor), }; if (kind !== 'field') { this.disallowProperty(elementObject, 'initializer', 'A method descriptor'); } else { this.disallowProperty( descriptor, 'get', 'The property descriptor of a field descriptor' ); this.disallowProperty( descriptor, 'set', 'The property descriptor of a field descriptor' ); this.disallowProperty( descriptor, 'value', 'The property descriptor of a field descriptor' ); element.initializer = elementObject.initializer; } return element; }, toElementFinisherExtras: function(elementObject) { var element = this.toElementDescriptor(elementObject); var finisher = _optionalCallableProperty(elementObject, 'finisher'); var extras = this.toElementDescriptors(elementObject.extras); return { element: element, finisher: finisher, extras: extras, }; }, fromClassDescriptor: function(elements) { var obj = { kind: 'class', elements: elements.map(this.fromElementDescriptor, this), }; var desc = { value: 'Descriptor', configurable: true, }; Object.defineProperty(obj, Symbol.toStringTag, desc); return obj; }, toClassDescriptor: function(obj) { var kind = String(obj.kind); if (kind !== 'class') { throw new TypeError( 'A class descriptor\'s .kind property must be "class", but a decorator' + ' created a class descriptor with .kind "' + kind + '"' ); } this.disallowProperty(obj, 'key', 'A class descriptor'); this.disallowProperty(obj, 'placement', 'A class descriptor'); this.disallowProperty(obj, 'descriptor', 'A class descriptor'); this.disallowProperty(obj, 'initializer', 'A class descriptor'); this.disallowProperty(obj, 'extras', 'A class descriptor'); var finisher = _optionalCallableProperty(obj, 'finisher'); var elements = this.toElementDescriptors(obj.elements); return { elements: elements, finisher: finisher, }; }, runClassFinishers: function(constructor, finishers) { for (var i = 0; i < finishers.length; i++) { var newConstructor = (0, finishers[i])(constructor); if (newConstructor !== undefined) { if (typeof newConstructor !== 'function') { throw new TypeError('Finishers must return a constructor.'); } constructor = newConstructor; } } return constructor; }, disallowProperty: function(obj, name, objectType) { if (obj[name] !== undefined) { throw new TypeError(objectType + " can't have a ." + name + ' property.'); } }, }; return api; } function _createElementDescriptor(def) { var key = _toPropertyKey(def.key); var descriptor; if (def.kind === 'method') { descriptor = { value: def.value, writable: true, configurable: true, enumerable: false, }; } else if (def.kind === 'get') { descriptor = { get: def.value, configurable: true, enumerable: false, }; } else if (def.kind === 'set') { descriptor = { set: def.value, configurable: true, enumerable: false, }; } else if (def.kind === 'field') { descriptor = { configurable: true, writable: true, enumerable: true, }; } var element = { kind: def.kind === 'field' ? 'field' : 'method', key: key, placement: def.static ? 'static' : def.kind === 'field' ? 'own' : 'prototype', descriptor: descriptor, }; if (def.decorators) element.decorators = def.decorators; if (def.kind === 'field') element.initializer = def.value; return element; } function _coalesceGetterSetter(element, other) { if (element.descriptor.get !== undefined) { other.descriptor.get = element.descriptor.get; } else { other.descriptor.set = element.descriptor.set; } } function _coalesceClassElements(elements) { var newElements = []; var isSameElement = function(other) { return ( other.kind === 'method' && other.key === element.key && other.placement === element.placement ); }; for (var i = 0; i < elements.length; i++) { var element = elements[i]; var other; if (element.kind === 'method' && (other = newElements.find(isSameElement))) { if (_isDataDescriptor(element.descriptor) || _isDataDescriptor(other.descriptor)) { if (_hasDecorators(element) || _hasDecorators(other)) { throw new ReferenceError( 'Duplicated methods (' + element.key + ") can't be decorated." ); } other.descriptor = element.descriptor; } else { if (_hasDecorators(element)) { if (_hasDecorators(other)) { throw new ReferenceError( "Decorators can't be placed on different accessors with for " + 'the same property (' + element.key + ').' ); } other.decorators = element.decorators; } _coalesceGetterSetter(element, other); } } else { newElements.push(element); } } return newElements; } function _hasDecorators(element) { return element.decorators && element.decorators.length; } function _isDataDescriptor(desc) { return desc !== undefined && !(desc.value === undefined && desc.writable === undefined); } function _optionalCallableProperty(obj, name) { var value = obj[name]; if (value !== undefined && typeof value !== 'function') { throw new TypeError("Expected '" + name + "' to be a function"); } return value; } function _classPrivateMethodGet(receiver, privateSet, fn) { if (!privateSet.has(receiver)) { throw new TypeError('attempted to get private field on non-instance'); } return fn; } function _classPrivateMethodSet() { throw new TypeError('attempted to reassign private method'); } function _wrapRegExp(re, groups) { _wrapRegExp = function(re, groups) { return new BabelRegExp(re, groups); }; var _RegExp = _wrapNativeSuper(RegExp); var _super = RegExp.prototype; var _groups = new WeakMap(); function BabelRegExp(re, groups) { var _this = _RegExp.call(this, re); _groups.set(_this, groups); return _this; } _inherits(BabelRegExp, _RegExp); BabelRegExp.prototype.exec = function(str) { var result = _super.exec.call(this, str); if (result) result.groups = buildGroups(result, this); return result; }; BabelRegExp.prototype[Symbol.replace] = function(str, substitution) { if (typeof substitution === 'string') { var groups = _groups.get(this); return _super[Symbol.replace].call( this, str, substitution.replace(/\$<([^>]+)>/g, function(_, name) { return '$' + groups[name]; }) ); } else if (typeof substitution === 'function') { var _this = this; return _super[Symbol.replace].call(this, str, function() { var args = []; args.push.apply(args, arguments); if (typeof args[args.length - 1] !== 'object') { args.push(buildGroups(args, _this)); } return substitution.apply(this, args); }); } else { return _super[Symbol.replace].call(this, str, substitution); } }; function buildGroups(result, re) { var g = _groups.get(re); return Object.keys(g).reduce(function(groups, name) { groups[name] = result[g[name]]; return groups; }, Object.create(null)); } return _wrapRegExp.apply(this, arguments); } var arrayRemove = function arrayRemove(arr, index) { return arr.splice(index, 1); }; var run = function run(cb, sync) { if (sync) { cb(); } else if (document.hidden) { Promise.resolve(1).then(cb); } else { setTimeout(cb, 0); } }; var on = function on() { var listeners = []; var off = function off(event, cb) { arrayRemove( listeners, listeners.findIndex(function(listener) { return listener.event === event && (listener.cb === cb || !cb); }) ); }; var _fire = function fire(event, args, sync) { listeners .filter(function(listener) { return listener.event === event; }) .map(function(listener) { return listener.cb; }) .forEach(function(cb) { return run(function() { return cb.apply(void 0, _toConsumableArray(args)); }, sync); }); }; return { fireSync: function fireSync(event) { for ( var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++ ) { args[_key - 1] = arguments[_key]; } _fire(event, args, true); }, fire: function fire(event) { for ( var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++ ) { args[_key2 - 1] = arguments[_key2]; } _fire(event, args, false); }, on: function on(event, cb) { listeners.push({ event: event, cb: cb }); }, onOnce: function onOnce(event, _cb) { listeners.push({ event: event, cb: function cb() { off(event, _cb); _cb.apply(void 0, arguments); }, }); }, off: off, }; }; var copyObjectPropertiesToObject = function copyObjectPropertiesToObject( src, target, excluded ) { Object.getOwnPropertyNames(src) .filter(function(property) { return !excluded.includes(property); }) .forEach(function(key) { return Object.defineProperty( target, key, Object.getOwnPropertyDescriptor(src, key) ); }); }; var PRIVATE = [ 'fire', 'process', 'revert', 'load', 'on', 'off', 'onOnce', 'retryLoad', 'extend', 'archive', 'archived', 'release', 'released', 'requestProcessing', 'freeze', ]; var createItemAPI = function createItemAPI(item) { var api = {}; copyObjectPropertiesToObject(item, api, PRIVATE); return api; }; var removeReleasedItems = function removeReleasedItems(items) { items.forEach(function(item, index) { if (item.released) { arrayRemove(items, index); } }); }; var ItemStatus = { INIT: 1, IDLE: 2, PROCESSING_QUEUED: 9, PROCESSING: 3, PROCESSING_COMPLETE: 5, PROCESSING_ERROR: 6, PROCESSING_REVERT_ERROR: 10, LOADING: 7, LOAD_ERROR: 8, }; var FileOrigin = { INPUT: 1, LIMBO: 2, LOCAL: 3, }; var getNonNumeric = function getNonNumeric(str) { return /[^0-9]+/.exec(str); }; var getDecimalSeparator = function getDecimalSeparator() { return getNonNumeric((1.1).toLocaleString())[0]; }; var getThousandsSeparator = function getThousandsSeparator() { // Added for browsers that do not return the thousands separator (happend on native browser Android 4.4.4) // We check against the normal toString output and if they're the same return a comma when decimal separator is a dot var decimalSeparator = getDecimalSeparator(); var thousandsStringWithSeparator = (1000.0).toLocaleString(); var thousandsStringWithoutSeparator = (1000.0).toString(); if (thousandsStringWithSeparator !== thousandsStringWithoutSeparator) { return getNonNumeric(thousandsStringWithSeparator)[0]; } return decimalSeparator === '.' ? ',' : '.'; }; var Type = { BOOLEAN: 'boolean', INT: 'int', NUMBER: 'number', STRING: 'string', ARRAY: 'array', OBJECT: 'object', FUNCTION: 'function', ACTION: 'action', SERVER_API: 'serverapi', REGEX: 'regex', }; // all registered filters var filters = []; // loops over matching filters and passes options to each filter, returning the mapped results var applyFilterChain = function applyFilterChain(key, value, utils) { return new Promise(function(resolve, reject) { // find matching filters for this key var matchingFilters = filters .filter(function(f) { return f.key === key; }) .map(function(f) { return f.cb; }); // resolve now if (matchingFilters.length === 0) { resolve(value); return; } // first filter to kick things of var initialFilter = matchingFilters.shift(); // chain filters matchingFilters .reduce( // loop over promises passing value to next promise function(current, next) { return current.then(function(value) { return next(value, utils); }); }, // call initial filter, will return a promise initialFilter(value, utils) // all executed ) .then(function(value) { return resolve(value); }) .catch(function(error) { return reject(error); }); }); }; var applyFilters = function applyFilters(key, value, utils) { return filters .filter(function(f) { return f.key === key; }) .map(function(f) { return f.cb(value, utils); }); }; // adds a new filter to the list var addFilter = function addFilter(key, cb) { return filters.push({ key: key, cb: cb }); }; var extendDefaultOptions = function extendDefaultOptions(additionalOptions) { return Object.assign(defaultOptions, additionalOptions); }; var getOptions = function getOptions() { return Object.assign({}, defaultOptions); }; var setOptions = function setOptions(opts) { forin(opts, function(key, value) { // key does not exist, so this option cannot be set if (!defaultOptions[key]) { return; } defaultOptions[key][0] = getValueByType( value, defaultOptions[key][0], defaultOptions[key][1] ); }); }; // default options on app var defaultOptions = { // the id to add to the root element id: [null, Type.STRING], // input field name to use name: ['filepond', Type.STRING], // disable the field disabled: [false, Type.BOOLEAN], // classname to put on wrapper className: [null, Type.STRING], // is the field required required: [false, Type.BOOLEAN], // Allow media capture when value is set captureMethod: [null, Type.STRING], // - "camera", "microphone" or "camcorder", // - Does not work with multiple on apple devices // - If set, acceptedFileTypes must be made to match with media wildcard "image/*", "audio/*" or "video/*" // sync `acceptedFileTypes` property with `accept` attribute allowSyncAcceptAttribute: [true, Type.BOOLEAN], // Feature toggles allowDrop: [true, Type.BOOLEAN], // Allow dropping of files allowBrowse: [true, Type.BOOLEAN], // Allow browsing the file system allowPaste: [true, Type.BOOLEAN], // Allow pasting files allowMultiple: [false, Type.BOOLEAN], // Allow multiple files (disabled by default, as multiple attribute is also required on input to allow multiple) allowReplace: [true, Type.BOOLEAN], // Allow dropping a file on other file to replace it (only works when multiple is set to false) allowRevert: [true, Type.BOOLEAN], // Allows user to revert file upload allowRemove: [true, Type.BOOLEAN], // Allow user to remove a file allowProcess: [true, Type.BOOLEAN], // Allows user to process a file, when set to false, this removes the file upload button allowReorder: [false, Type.BOOLEAN], // Allow reordering of files allowDirectoriesOnly: [false, Type.BOOLEAN], // Allow only selecting directories with browse (no support for filtering dnd at this point) // Try store file if `server` not set storeAsFile: [false, Type.BOOLEAN], // Revert mode forceRevert: [false, Type.BOOLEAN], // Set to 'force' to require the file to be reverted before removal // Input requirements maxFiles: [null, Type.INT], // Max number of files checkValidity: [false, Type.BOOLEAN], // Enables custom validity messages // Where to put file itemInsertLocationFreedom: [true, Type.BOOLEAN], // Set to false to always add items to begin or end of list itemInsertLocation: ['before', Type.STRING], // Default index in list to add items that have been dropped at the top of the list itemInsertInterval: [75, Type.INT], // Drag 'n Drop related dropOnPage: [false, Type.BOOLEAN], // Allow dropping of files anywhere on page (prevents browser from opening file if dropped outside of Up) dropOnElement: [true, Type.BOOLEAN], // Drop needs to happen on element (set to false to also load drops outside of Up) dropValidation: [false, Type.BOOLEAN], // Enable or disable validating files on drop ignoredFiles: [['.ds_store', 'thumbs.db', 'desktop.ini'], Type.ARRAY], // Upload related instantUpload: [true, Type.BOOLEAN], // Should upload files immediately on drop maxParallelUploads: [2, Type.INT], // Maximum files to upload in parallel allowMinimumUploadDuration: [true, Type.BOOLEAN], // if true uploads take at least 750 ms, this ensures the user sees the upload progress giving trust the upload actually happened // Chunks chunkUploads: [false, Type.BOOLEAN], // Enable chunked uploads chunkForce: [false, Type.BOOLEAN], // Force use of chunk uploads even for files smaller than chunk size chunkSize: [5000000, Type.INT], // Size of chunks (5MB default) chunkRetryDelays: [[500, 1000, 3000], Type.ARRAY], // Amount of times to retry upload of a chunk when it fails // The server api end points to use for uploading (see docs) server: [null, Type.SERVER_API], // File size calculations, can set to 1024, this is only used for display, properties use file size base 1000 fileSizeBase: [1000, Type.INT], // Labels and status messages labelFileSizeBytes: ['bytes', Type.STRING], labelFileSizeKilobytes: ['KB', Type.STRING], labelFileSizeMegabytes: ['MB', Type.STRING], labelFileSizeGigabytes: ['GB', Type.STRING], labelDecimalSeparator: [getDecimalSeparator(), Type.STRING], // Default is locale separator labelThousandsSeparator: [getThousandsSeparator(), Type.STRING], // Default is locale separator labelIdle: [ 'Drag & Drop your files or Browse', Type.STRING, ], labelInvalidField: ['Field contains invalid files', Type.STRING], labelFileWaitingForSize: ['Waiting for size', Type.STRING], labelFileSizeNotAvailable: ['Size not available', Type.STRING], labelFileCountSingular: ['file in list', Type.STRING], labelFileCountPlural: ['files in list', Type.STRING], labelFileLoading: ['Loading', Type.STRING], labelFileAdded: ['Added', Type.STRING], // assistive only labelFileLoadError: ['Error during load', Type.STRING], labelFileRemoved: ['Removed', Type.STRING], // assistive only labelFileRemoveError: ['Error during remove', Type.STRING], labelFileProcessing: ['Uploading', Type.STRING], labelFileProcessingComplete: ['Upload complete', Type.STRING], labelFileProcessingAborted: ['Upload cancelled', Type.STRING], labelFileProcessingError: ['Error during upload', Type.STRING], labelFileProcessingRevertError: ['Error during revert', Type.STRING], labelTapToCancel: ['tap to cancel', Type.STRING], labelTapToRetry: ['tap to retry', Type.STRING], labelTapToUndo: ['tap to undo', Type.STRING], labelButtonRemoveItem: ['Remove', Type.STRING], labelButtonAbortItemLoad: ['Abort', Type.STRING], labelButtonRetryItemLoad: ['Retry', Type.STRING], labelButtonAbortItemProcessing: ['Cancel', Type.STRING], labelButtonUndoItemProcessing: ['Undo', Type.STRING], labelButtonRetryItemProcessing: ['Retry', Type.STRING], labelButtonProcessItem: ['Upload', Type.STRING], // make sure width and height plus viewpox are even numbers so icons are nicely centered iconRemove: [ '', Type.STRING, ], iconProcess: [ '', Type.STRING, ], iconRetry: [ '', Type.STRING, ], iconUndo: [ '', Type.STRING, ], iconDone: [ '', Type.STRING, ], // event handlers oninit: [null, Type.FUNCTION], onwarning: [null, Type.FUNCTION], onerror: [null, Type.FUNCTION], onactivatefile: [null, Type.FUNCTION], oninitfile: [null, Type.FUNCTION], onaddfilestart: [null, Type.FUNCTION], onaddfileprogress: [null, Type.FUNCTION], onaddfile: [null, Type.FUNCTION], onprocessfilestart: [null, Type.FUNCTION], onprocessfileprogress: [null, Type.FUNCTION], onprocessfileabort: [null, Type.FUNCTION], onprocessfilerevert: [null, Type.FUNCTION], onprocessfile: [null, Type.FUNCTION], onprocessfiles: [null, Type.FUNCTION], onremovefile: [null, Type.FUNCTION], onpreparefile: [null, Type.FUNCTION], onupdatefiles: [null, Type.FUNCTION], onreorderfiles: [null, Type.FUNCTION], // hooks beforeDropFile: [null, Type.FUNCTION], beforeAddFile: [null, Type.FUNCTION], beforeRemoveFile: [null, Type.FUNCTION], beforePrepareFile: [null, Type.FUNCTION], // styles stylePanelLayout: [null, Type.STRING], // null 'integrated', 'compact', 'circle' stylePanelAspectRatio: [null, Type.STRING], // null or '3:2' or 1 styleItemPanelAspectRatio: [null, Type.STRING], styleButtonRemoveItemPosition: ['left', Type.STRING], styleButtonProcessItemPosition: ['right', Type.STRING], styleLoadIndicatorPosition: ['right', Type.STRING], styleProgressIndicatorPosition: ['right', Type.STRING], styleButtonRemoveItemAlign: [false, Type.BOOLEAN], // custom initial files array files: [[], Type.ARRAY], // show support by displaying credits credits: [['https://filepond.com', 'Powered by FilePond'], Type.ARRAY], }; var getItemByQuery = function getItemByQuery(items, query) { // just return first index if (isEmpty(query)) { return items[0] || null; } // query is index if (isInt(query)) { return items[query] || null; } // if query is item, get the id if (typeof query === 'object') { query = query.id; } // assume query is a string and return item by id return ( items.find(function(item) { return item.id === query; }) || null ); }; var getNumericAspectRatioFromString = function getNumericAspectRatioFromString(aspectRatio) { if (isEmpty(aspectRatio)) { return aspectRatio; } if (/:/.test(aspectRatio)) { var parts = aspectRatio.split(':'); return parts[1] / parts[0]; } return parseFloat(aspectRatio); }; var getActiveItems = function getActiveItems(items) { return items.filter(function(item) { return !item.archived; }); }; var Status = { EMPTY: 0, IDLE: 1, // waiting ERROR: 2, // a file is in error state BUSY: 3, // busy processing or loading READY: 4, // all files uploaded }; var res = null; var canUpdateFileInput = function canUpdateFileInput() { if (res === null) { try { var dataTransfer = new DataTransfer(); dataTransfer.items.add(new File(['hello world'], 'This_Works.txt')); var el = document.createElement('input'); el.setAttribute('type', 'file'); el.files = dataTransfer.files; res = el.files.length === 1; } catch (err) { res = false; } } return res; }; var ITEM_ERROR = [ ItemStatus.LOAD_ERROR, ItemStatus.PROCESSING_ERROR, ItemStatus.PROCESSING_REVERT_ERROR, ]; var ITEM_BUSY = [ ItemStatus.LOADING, ItemStatus.PROCESSING, ItemStatus.PROCESSING_QUEUED, ItemStatus.INIT, ]; var ITEM_READY = [ItemStatus.PROCESSING_COMPLETE]; var isItemInErrorState = function isItemInErrorState(item) { return ITEM_ERROR.includes(item.status); }; var isItemInBusyState = function isItemInBusyState(item) { return ITEM_BUSY.includes(item.status); }; var isItemInReadyState = function isItemInReadyState(item) { return ITEM_READY.includes(item.status); }; var isAsync = function isAsync(state) { return ( isObject(state.options.server) && (isObject(state.options.server.process) || isFunction(state.options.server.process)) ); }; var queries = function queries(state) { return { GET_STATUS: function GET_STATUS() { var items = getActiveItems(state.items); var EMPTY = Status.EMPTY, ERROR = Status.ERROR, BUSY = Status.BUSY, IDLE = Status.IDLE, READY = Status.READY; if (items.length === 0) return EMPTY; if (items.some(isItemInErrorState)) return ERROR; if (items.some(isItemInBusyState)) return BUSY; if (items.some(isItemInReadyState)) return READY; return IDLE; }, GET_ITEM: function GET_ITEM(query) { return getItemByQuery(state.items, query); }, GET_ACTIVE_ITEM: function GET_ACTIVE_ITEM(query) { return getItemByQuery(getActiveItems(state.items), query); }, GET_ACTIVE_ITEMS: function GET_ACTIVE_ITEMS() { return getActiveItems(state.items); }, GET_ITEMS: function GET_ITEMS() { return state.items; }, GET_ITEM_NAME: function GET_ITEM_NAME(query) { var item = getItemByQuery(state.items, query); return item ? item.filename : null; }, GET_ITEM_SIZE: function GET_ITEM_SIZE(query) { var item = getItemByQuery(state.items, query); return item ? item.fileSize : null; }, GET_STYLES: function GET_STYLES() { return Object.keys(state.options) .filter(function(key) { return /^style/.test(key); }) .map(function(option) { return { name: option, value: state.options[option], }; }); }, GET_PANEL_ASPECT_RATIO: function GET_PANEL_ASPECT_RATIO() { var isShapeCircle = /circle/.test(state.options.stylePanelLayout); var aspectRatio = isShapeCircle ? 1 : getNumericAspectRatioFromString(state.options.stylePanelAspectRatio); return aspectRatio; }, GET_ITEM_PANEL_ASPECT_RATIO: function GET_ITEM_PANEL_ASPECT_RATIO() { return state.options.styleItemPanelAspectRatio; }, GET_ITEMS_BY_STATUS: function GET_ITEMS_BY_STATUS(status) { return getActiveItems(state.items).filter(function(item) { return item.status === status; }); }, GET_TOTAL_ITEMS: function GET_TOTAL_ITEMS() { return getActiveItems(state.items).length; }, SHOULD_UPDATE_FILE_INPUT: function SHOULD_UPDATE_FILE_INPUT() { return state.options.storeAsFile && canUpdateFileInput() && !isAsync(state); }, IS_ASYNC: function IS_ASYNC() { return isAsync(state); }, GET_FILE_SIZE_LABELS: function GET_FILE_SIZE_LABELS(query) { return { labelBytes: query('GET_LABEL_FILE_SIZE_BYTES') || undefined, labelKilobytes: query('GET_LABEL_FILE_SIZE_KILOBYTES') || undefined, labelMegabytes: query('GET_LABEL_FILE_SIZE_MEGABYTES') || undefined, labelGigabytes: query('GET_LABEL_FILE_SIZE_GIGABYTES') || undefined, }; }, }; }; var hasRoomForItem = function hasRoomForItem(state) { var count = getActiveItems(state.items).length; // if cannot have multiple items, to add one item it should currently not contain items if (!state.options.allowMultiple) { return count === 0; } // if allows multiple items, we check if a max item count has been set, if not, there's no limit var maxFileCount = state.options.maxFiles; if (maxFileCount === null) { return true; } // we check if the current count is smaller than the max count, if so, another file can still be added if (count < maxFileCount) { return true; } // no more room for another file return false; }; var limit = function limit(value, min, max) { return Math.max(Math.min(max, value), min); }; var arrayInsert = function arrayInsert(arr, index, item) { return arr.splice(index, 0, item); }; var insertItem = function insertItem(items, item, index) { if (isEmpty(item)) { return null; } // if index is undefined, append if (typeof index === 'undefined') { items.push(item); return item; } // limit the index to the size of the items array index = limit(index, 0, items.length); // add item to array arrayInsert(items, index, item); // expose return item; }; var isBase64DataURI = function isBase64DataURI(str) { return /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*)\s*$/i.test( str ); }; var getFilenameFromURL = function getFilenameFromURL(url) { return ('' + url) .split('/') .pop() .split('?') .shift(); }; var getExtensionFromFilename = function getExtensionFromFilename(name) { return name.split('.').pop(); }; var guesstimateExtension = function guesstimateExtension(type) { // if no extension supplied, exit here if (typeof type !== 'string') { return ''; } // get subtype var subtype = type.split('/').pop(); // is svg subtype if (/svg/.test(subtype)) { return 'svg'; } if (/zip|compressed/.test(subtype)) { return 'zip'; } if (/plain/.test(subtype)) { return 'txt'; } if (/msword/.test(subtype)) { return 'doc'; } // if is valid subtype if (/[a-z]+/.test(subtype)) { // always use jpg extension if (subtype === 'jpeg') { return 'jpg'; } // return subtype return subtype; } return ''; }; var leftPad = function leftPad(value) { var padding = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; return (padding + value).slice(-padding.length); }; var getDateString = function getDateString() { var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Date(); return ( date.getFullYear() + '-' + leftPad(date.getMonth() + 1, '00') + '-' + leftPad(date.getDate(), '00') + '_' + leftPad(date.getHours(), '00') + '-' + leftPad(date.getMinutes(), '00') + '-' + leftPad(date.getSeconds(), '00') ); }; var getFileFromBlob = function getFileFromBlob(blob, filename) { var type = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; var extension = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; var file = typeof type === 'string' ? blob.slice(0, blob.size, type) : blob.slice(0, blob.size, blob.type); file.lastModifiedDate = new Date(); // copy relative path if (blob._relativePath) file._relativePath = blob._relativePath; // if blob has name property, use as filename if no filename supplied if (!isString(filename)) { filename = getDateString(); } // if filename supplied but no extension and filename has extension if (filename && extension === null && getExtensionFromFilename(filename)) { file.name = filename; } else { extension = extension || guesstimateExtension(file.type); file.name = filename + (extension ? '.' + extension : ''); } return file; }; var getBlobBuilder = function getBlobBuilder() { return (window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder); }; var createBlob = function createBlob(arrayBuffer, mimeType) { var BB = getBlobBuilder(); if (BB) { var bb = new BB(); bb.append(arrayBuffer); return bb.getBlob(mimeType); } return new Blob([arrayBuffer], { type: mimeType, }); }; var getBlobFromByteStringWithMimeType = function getBlobFromByteStringWithMimeType( byteString, mimeType ) { var ab = new ArrayBuffer(byteString.length); var ia = new Uint8Array(ab); for (var i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } return createBlob(ab, mimeType); }; var getMimeTypeFromBase64DataURI = function getMimeTypeFromBase64DataURI(dataURI) { return (/^data:(.+);/.exec(dataURI) || [])[1] || null; }; var getBase64DataFromBase64DataURI = function getBase64DataFromBase64DataURI(dataURI) { // get data part of string (remove data:image/jpeg...,) var data = dataURI.split(',')[1]; // remove any whitespace as that causes InvalidCharacterError in IE return data.replace(/\s/g, ''); }; var getByteStringFromBase64DataURI = function getByteStringFromBase64DataURI(dataURI) { return atob(getBase64DataFromBase64DataURI(dataURI)); }; var getBlobFromBase64DataURI = function getBlobFromBase64DataURI(dataURI) { var mimeType = getMimeTypeFromBase64DataURI(dataURI); var byteString = getByteStringFromBase64DataURI(dataURI); return getBlobFromByteStringWithMimeType(byteString, mimeType); }; var getFileFromBase64DataURI = function getFileFromBase64DataURI(dataURI, filename, extension) { return getFileFromBlob(getBlobFromBase64DataURI(dataURI), filename, null, extension); }; var getFileNameFromHeader = function getFileNameFromHeader(header) { // test if is content disposition header, if not exit if (!/^content-disposition:/i.test(header)) return null; // get filename parts var matches = header .split(/filename=|filename\*=.+''/) .splice(1) .map(function(name) { return name.trim().replace(/^["']|[;"']{0,2}$/g, ''); }) .filter(function(name) { return name.length; }); return matches.length ? decodeURI(matches[matches.length - 1]) : null; }; var getFileSizeFromHeader = function getFileSizeFromHeader(header) { if (/content-length:/i.test(header)) { var size = header.match(/[0-9]+/)[0]; return size ? parseInt(size, 10) : null; } return null; }; var getTranfserIdFromHeader = function getTranfserIdFromHeader(header) { if (/x-content-transfer-id:/i.test(header)) { var id = (header.split(':')[1] || '').trim(); return id || null; } return null; }; var getFileInfoFromHeaders = function getFileInfoFromHeaders(headers) { var info = { source: null, name: null, size: null, }; var rows = headers.split('\n'); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for ( var _iterator = rows[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true ) { var header = _step.value; var name = getFileNameFromHeader(header); if (name) { info.name = name; continue; } var size = getFileSizeFromHeader(header); if (size) { info.size = size; continue; } var source = getTranfserIdFromHeader(header); if (source) { info.source = source; continue; } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return != null) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return info; }; var createFileLoader = function createFileLoader(fetchFn) { var state = { source: null, complete: false, progress: 0, size: null, timestamp: null, duration: 0, request: null, }; var getProgress = function getProgress() { return state.progress; }; var abort = function abort() { if (state.request && state.request.abort) { state.request.abort(); } }; // load source var load = function load() { // get quick reference var source = state.source; api.fire('init', source); // Load Files if (source instanceof File) { api.fire('load', source); } else if (source instanceof Blob) { // Load blobs, set default name to current date api.fire('load', getFileFromBlob(source, source.name)); } else if (isBase64DataURI(source)) { // Load base 64, set default name to current date api.fire('load', getFileFromBase64DataURI(source)); } else { // Deal as if is external URL, let's load it! loadURL(source); } }; // loads a url var loadURL = function loadURL(url) { // is remote url and no fetch method supplied if (!fetchFn) { api.fire('error', { type: 'error', body: "Can't load URL", code: 400, }); return; } // set request start state.timestamp = Date.now(); // load file state.request = fetchFn( url, function(response) { // update duration state.duration = Date.now() - state.timestamp; // done! state.complete = true; // turn blob response into a file if (response instanceof Blob) { response = getFileFromBlob( response, response.name || getFilenameFromURL(url) ); } api.fire( 'load', // if has received blob, we go with blob, if no response, we return null response instanceof Blob ? response : response ? response.body : null ); }, function(error) { api.fire( 'error', typeof error === 'string' ? { type: 'error', code: 0, body: error, } : error ); }, function(computable, current, total) { // collected some meta data already if (total) { state.size = total; } // update duration state.duration = Date.now() - state.timestamp; // if we can't compute progress, we're not going to fire progress events if (!computable) { state.progress = null; return; } // update progress percentage state.progress = current / total; // expose api.fire('progress', state.progress); }, function() { api.fire('abort'); }, function(response) { var fileinfo = getFileInfoFromHeaders( typeof response === 'string' ? response : response.headers ); api.fire('meta', { size: state.size || fileinfo.size, filename: fileinfo.name, source: fileinfo.source, }); } ); }; var api = Object.assign({}, on(), { setSource: function setSource(source) { return (state.source = source); }, getProgress: getProgress, // file load progress abort: abort, // abort file load load: load, // start load }); return api; }; var isGet = function isGet(method) { return /GET|HEAD/.test(method); }; var sendRequest = function sendRequest(data, url, options) { var api = { onheaders: function onheaders() {}, onprogress: function onprogress() {}, onload: function onload() {}, ontimeout: function ontimeout() {}, onerror: function onerror() {}, onabort: function onabort() {}, abort: function abort() { aborted = true; xhr.abort(); }, }; // timeout identifier, only used when timeout is defined var aborted = false; var headersReceived = false; // set default options options = Object.assign( { method: 'POST', headers: {}, withCredentials: false, }, options ); // encode url url = encodeURI(url); // if method is GET, add any received data to url if (isGet(options.method) && data) { url = '' + url + encodeURIComponent(typeof data === 'string' ? data : JSON.stringify(data)); } // create request var xhr = new XMLHttpRequest(); // progress of load var process = isGet(options.method) ? xhr : xhr.upload; process.onprogress = function(e) { // no progress event when aborted ( onprogress is called once after abort() ) if (aborted) { return; } api.onprogress(e.lengthComputable, e.loaded, e.total); }; // tries to get header info to the app as fast as possible xhr.onreadystatechange = function() { // not interesting in these states ('unsent' and 'openend' as they don't give us any additional info) if (xhr.readyState < 2) { return; } // no server response if (xhr.readyState === 4 && xhr.status === 0) { return; } if (headersReceived) { return; } headersReceived = true; // we've probably received some useful data in response headers api.onheaders(xhr); }; // load successful xhr.onload = function() { // is classified as valid response if (xhr.status >= 200 && xhr.status < 300) { api.onload(xhr); } else { api.onerror(xhr); } }; // error during load xhr.onerror = function() { return api.onerror(xhr); }; // request aborted xhr.onabort = function() { aborted = true; api.onabort(); }; // request timeout xhr.ontimeout = function() { return api.ontimeout(xhr); }; // open up open up! xhr.open(options.method, url, true); // set timeout if defined (do it after open so IE11 plays ball) if (isInt(options.timeout)) { xhr.timeout = options.timeout; } // add headers Object.keys(options.headers).forEach(function(key) { var value = unescape(encodeURIComponent(options.headers[key])); xhr.setRequestHeader(key, value); }); // set type of response if (options.responseType) { xhr.responseType = options.responseType; } // set credentials if (options.withCredentials) { xhr.withCredentials = true; } // let's send our data xhr.send(data); return api; }; var createResponse = function createResponse(type, code, body, headers) { return { type: type, code: code, body: body, headers: headers, }; }; var createTimeoutResponse = function createTimeoutResponse(cb) { return function(xhr) { cb(createResponse('error', 0, 'Timeout', xhr.getAllResponseHeaders())); }; }; var hasQS = function hasQS(str) { return /\?/.test(str); }; var buildURL = function buildURL() { var url = ''; for (var _len = arguments.length, parts = new Array(_len), _key = 0; _key < _len; _key++) { parts[_key] = arguments[_key]; } parts.forEach(function(part) { url += hasQS(url) && hasQS(part) ? part.replace(/\?/, '&') : part; }); return url; }; var createFetchFunction = function createFetchFunction() { var apiUrl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var action = arguments.length > 1 ? arguments[1] : undefined; // custom handler (should also handle file, load, error, progress and abort) if (typeof action === 'function') { return action; } // no action supplied if (!action || !isString(action.url)) { return null; } // set onload hanlder var onload = action.onload || function(res) { return res; }; var onerror = action.onerror || function(res) { return null; }; // internal handler return function(url, load, error, progress, abort, headers) { // do local or remote request based on if the url is external var request = sendRequest( url, buildURL(apiUrl, action.url), Object.assign({}, action, { responseType: 'blob', }) ); request.onload = function(xhr) { // get headers var headers = xhr.getAllResponseHeaders(); // get filename var filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url); // create response load( createResponse( 'load', xhr.status, action.method === 'HEAD' ? null : getFileFromBlob(onload(xhr.response), filename), headers ) ); }; request.onerror = function(xhr) { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.onheaders = function(xhr) { headers(createResponse('headers', xhr.status, null, xhr.getAllResponseHeaders())); }; request.ontimeout = createTimeoutResponse(error); request.onprogress = progress; request.onabort = abort; // should return request return request; }; }; var ChunkStatus = { QUEUED: 0, COMPLETE: 1, PROCESSING: 2, ERROR: 3, WAITING: 4, }; /* function signature: (file, metadata, load, error, progress, abort, transfer, options) => { return { abort:() => {} } } */ // apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options var processFileChunked = function processFileChunked( apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options ) { // all chunks var chunks = []; var chunkTransferId = options.chunkTransferId, chunkServer = options.chunkServer, chunkSize = options.chunkSize, chunkRetryDelays = options.chunkRetryDelays; // default state var state = { serverId: chunkTransferId, aborted: false, }; // set onload handlers var ondata = action.ondata || function(fd) { return fd; }; var onload = action.onload || function(xhr, method) { return method === 'HEAD' ? xhr.getResponseHeader('Upload-Offset') : xhr.response; }; var onerror = action.onerror || function(res) { return null; }; // create server hook var requestTransferId = function requestTransferId(cb) { var formData = new FormData(); // add metadata under same name if (isObject(metadata)) formData.append(name, JSON.stringify(metadata)); var headers = typeof action.headers === 'function' ? action.headers(file, metadata) : Object.assign( {}, action.headers, { 'Upload-Length': file.size, } ); var requestParams = Object.assign({}, action, { headers: headers, }); // send request object var request = sendRequest( ondata(formData), buildURL(apiUrl, action.url), requestParams ); request.onload = function(xhr) { return cb(onload(xhr, requestParams.method)); }; request.onerror = function(xhr) { return error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.ontimeout = createTimeoutResponse(error); }; var requestTransferOffset = function requestTransferOffset(cb) { var requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId); var headers = typeof action.headers === 'function' ? action.headers(state.serverId) : Object.assign( {}, action.headers ); var requestParams = { headers: headers, method: 'HEAD', }; var request = sendRequest(null, requestUrl, requestParams); request.onload = function(xhr) { return cb(onload(xhr, requestParams.method)); }; request.onerror = function(xhr) { return error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.ontimeout = createTimeoutResponse(error); }; // create chunks var lastChunkIndex = Math.floor(file.size / chunkSize); for (var i = 0; i <= lastChunkIndex; i++) { var offset = i * chunkSize; var data = file.slice(offset, offset + chunkSize, 'application/offset+octet-stream'); chunks[i] = { index: i, size: data.size, offset: offset, data: data, file: file, progress: 0, retries: _toConsumableArray(chunkRetryDelays), status: ChunkStatus.QUEUED, error: null, request: null, timeout: null, }; } var completeProcessingChunks = function completeProcessingChunks() { return load(state.serverId); }; var canProcessChunk = function canProcessChunk(chunk) { return chunk.status === ChunkStatus.QUEUED || chunk.status === ChunkStatus.ERROR; }; var processChunk = function processChunk(chunk) { // processing is paused, wait here if (state.aborted) return; // get next chunk to process chunk = chunk || chunks.find(canProcessChunk); // no more chunks to process if (!chunk) { // all done? if ( chunks.every(function(chunk) { return chunk.status === ChunkStatus.COMPLETE; }) ) { completeProcessingChunks(); } // no chunk to handle return; } // now processing this chunk chunk.status = ChunkStatus.PROCESSING; chunk.progress = null; // allow parsing of formdata var ondata = chunkServer.ondata || function(fd) { return fd; }; var onerror = chunkServer.onerror || function(res) { return null; }; var onload = chunkServer.onload || function() {}; // send request object var requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId); var headers = typeof chunkServer.headers === 'function' ? chunkServer.headers(chunk) : Object.assign( {}, chunkServer.headers, { 'Content-Type': 'application/offset+octet-stream', 'Upload-Offset': chunk.offset, 'Upload-Length': file.size, 'Upload-Name': file.name, } ); var request = (chunk.request = sendRequest( ondata(chunk.data), requestUrl, Object.assign({}, chunkServer, { headers: headers, }) )); request.onload = function(xhr) { // allow hooking into request result onload(xhr, chunk.index, chunks.length); // done! chunk.status = ChunkStatus.COMPLETE; // remove request reference chunk.request = null; // start processing more chunks processChunks(); }; request.onprogress = function(lengthComputable, loaded, total) { chunk.progress = lengthComputable ? loaded : null; updateTotalProgress(); }; request.onerror = function(xhr) { chunk.status = ChunkStatus.ERROR; chunk.request = null; chunk.error = onerror(xhr.response) || xhr.statusText; if (!retryProcessChunk(chunk)) { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); } }; request.ontimeout = function(xhr) { chunk.status = ChunkStatus.ERROR; chunk.request = null; if (!retryProcessChunk(chunk)) { createTimeoutResponse(error)(xhr); } }; request.onabort = function() { chunk.status = ChunkStatus.QUEUED; chunk.request = null; abort(); }; }; var retryProcessChunk = function retryProcessChunk(chunk) { // no more retries left if (chunk.retries.length === 0) return false; // new retry chunk.status = ChunkStatus.WAITING; clearTimeout(chunk.timeout); chunk.timeout = setTimeout(function() { processChunk(chunk); }, chunk.retries.shift()); // we're going to retry return true; }; var updateTotalProgress = function updateTotalProgress() { // calculate total progress fraction var totalBytesTransfered = chunks.reduce(function(p, chunk) { if (p === null || chunk.progress === null) return null; return p + chunk.progress; }, 0); // can't compute progress if (totalBytesTransfered === null) return progress(false, 0, 0); // calculate progress values var totalSize = chunks.reduce(function(total, chunk) { return total + chunk.size; }, 0); // can update progress indicator progress(true, totalBytesTransfered, totalSize); }; // process new chunks var processChunks = function processChunks() { var totalProcessing = chunks.filter(function(chunk) { return chunk.status === ChunkStatus.PROCESSING; }).length; if (totalProcessing >= 1) return; processChunk(); }; var abortChunks = function abortChunks() { chunks.forEach(function(chunk) { clearTimeout(chunk.timeout); if (chunk.request) { chunk.request.abort(); } }); }; // let's go! if (!state.serverId) { requestTransferId(function(serverId) { // stop here if aborted, might have happened in between request and callback if (state.aborted) return; // pass back to item so we can use it if something goes wrong transfer(serverId); // store internally state.serverId = serverId; processChunks(); }); } else { requestTransferOffset(function(offset) { // stop here if aborted, might have happened in between request and callback if (state.aborted) return; // mark chunks with lower offset as complete chunks .filter(function(chunk) { return chunk.offset < offset; }) .forEach(function(chunk) { chunk.status = ChunkStatus.COMPLETE; chunk.progress = chunk.size; }); // continue processing processChunks(); }); } return { abort: function abort() { state.aborted = true; abortChunks(); }, }; }; /* function signature: (file, metadata, load, error, progress, abort) => { return { abort:() => {} } } */ var createFileProcessorFunction = function createFileProcessorFunction( apiUrl, action, name, options ) { return function(file, metadata, load, error, progress, abort, transfer) { // no file received if (!file) return; // if was passed a file, and we can chunk it, exit here var canChunkUpload = options.chunkUploads; var shouldChunkUpload = canChunkUpload && file.size > options.chunkSize; var willChunkUpload = canChunkUpload && (shouldChunkUpload || options.chunkForce); if (file instanceof Blob && willChunkUpload) return processFileChunked( apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options ); // set handlers var ondata = action.ondata || function(fd) { return fd; }; var onload = action.onload || function(res) { return res; }; var onerror = action.onerror || function(res) { return null; }; var headers = typeof action.headers === 'function' ? action.headers(file, metadata) || {} : Object.assign( {}, action.headers ); var requestParams = Object.assign({}, action, { headers: headers, }); // create formdata object var formData = new FormData(); // add metadata under same name if (isObject(metadata)) { formData.append(name, JSON.stringify(metadata)); } // Turn into an array of objects so no matter what the input, we can handle it the same way (file instanceof Blob ? [{ name: null, file: file }] : file).forEach(function(item) { formData.append( name, item.file, item.name === null ? item.file.name : '' + item.name + item.file.name ); }); // send request object var request = sendRequest( ondata(formData), buildURL(apiUrl, action.url), requestParams ); request.onload = function(xhr) { load( createResponse( 'load', xhr.status, onload(xhr.response), xhr.getAllResponseHeaders() ) ); }; request.onerror = function(xhr) { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.ontimeout = createTimeoutResponse(error); request.onprogress = progress; request.onabort = abort; // should return request return request; }; }; var createProcessorFunction = function createProcessorFunction() { var apiUrl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var action = arguments.length > 1 ? arguments[1] : undefined; var name = arguments.length > 2 ? arguments[2] : undefined; var options = arguments.length > 3 ? arguments[3] : undefined; // custom handler (should also handle file, load, error, progress and abort) if (typeof action === 'function') return function() { for ( var _len = arguments.length, params = new Array(_len), _key = 0; _key < _len; _key++ ) { params[_key] = arguments[_key]; } return action.apply(void 0, [name].concat(params, [options])); }; // no action supplied if (!action || !isString(action.url)) return null; // internal handler return createFileProcessorFunction(apiUrl, action, name, options); }; /* function signature: (uniqueFileId, load, error) => { } */ var createRevertFunction = function createRevertFunction() { var apiUrl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var action = arguments.length > 1 ? arguments[1] : undefined; // is custom implementation if (typeof action === 'function') { return action; } // no action supplied, return stub function, interface will work, but file won't be removed if (!action || !isString(action.url)) { return function(uniqueFileId, load) { return load(); }; } // set onload hanlder var onload = action.onload || function(res) { return res; }; var onerror = action.onerror || function(res) { return null; }; // internal implementation return function(uniqueFileId, load, error) { var request = sendRequest( uniqueFileId, apiUrl + action.url, action // contains method, headers and withCredentials properties ); request.onload = function(xhr) { load( createResponse( 'load', xhr.status, onload(xhr.response), xhr.getAllResponseHeaders() ) ); }; request.onerror = function(xhr) { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.ontimeout = createTimeoutResponse(error); return request; }; }; var getRandomNumber = function getRandomNumber() { var min = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; var max = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; return min + Math.random() * (max - min); }; var createPerceivedPerformanceUpdater = function createPerceivedPerformanceUpdater(cb) { var duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1000; var offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; var tickMin = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 25; var tickMax = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 250; var timeout = null; var start = Date.now(); var tick = function tick() { var runtime = Date.now() - start; var delay = getRandomNumber(tickMin, tickMax); if (runtime + delay > duration) { delay = runtime + delay - duration; } var progress = runtime / duration; if (progress >= 1 || document.hidden) { cb(1); return; } cb(progress); timeout = setTimeout(tick, delay); }; if (duration > 0) tick(); return { clear: function clear() { clearTimeout(timeout); }, }; }; var createFileProcessor = function createFileProcessor(processFn, options) { var state = { complete: false, perceivedProgress: 0, perceivedPerformanceUpdater: null, progress: null, timestamp: null, perceivedDuration: 0, duration: 0, request: null, response: null, }; var allowMinimumUploadDuration = options.allowMinimumUploadDuration; var process = function process(file, metadata) { var progressFn = function progressFn() { // we've not yet started the real download, stop here // the request might not go through, for instance, there might be some server trouble // if state.progress is null, the server does not allow computing progress and we show the spinner instead if (state.duration === 0 || state.progress === null) return; // as we're now processing, fire the progress event api.fire('progress', api.getProgress()); }; var completeFn = function completeFn() { state.complete = true; api.fire('load-perceived', state.response.body); }; // let's start processing api.fire('start'); // set request start state.timestamp = Date.now(); // create perceived performance progress indicator state.perceivedPerformanceUpdater = createPerceivedPerformanceUpdater( function(progress) { state.perceivedProgress = progress; state.perceivedDuration = Date.now() - state.timestamp; progressFn(); // if fake progress is done, and a response has been received, // and we've not yet called the complete method if (state.response && state.perceivedProgress === 1 && !state.complete) { // we done! completeFn(); } }, // random delay as in a list of files you start noticing // files uploading at the exact same speed allowMinimumUploadDuration ? getRandomNumber(750, 1500) : 0 ); // remember request so we can abort it later state.request = processFn( // the file to process file, // the metadata to send along metadata, // callbacks (load, error, progress, abort, transfer) // load expects the body to be a server id if // you want to make use of revert function(response) { // we put the response in state so we can access // it outside of this method state.response = isObject(response) ? response : { type: 'load', code: 200, body: '' + response, headers: {}, }; // update duration state.duration = Date.now() - state.timestamp; // force progress to 1 as we're now done state.progress = 1; // actual load is done let's share results api.fire('load', state.response.body); // we are really done // if perceived progress is 1 ( wait for perceived progress to complete ) // or if server does not support progress ( null ) if ( !allowMinimumUploadDuration || (allowMinimumUploadDuration && state.perceivedProgress === 1) ) { completeFn(); } }, // error is expected to be an object with type, code, body function(error) { // cancel updater state.perceivedPerformanceUpdater.clear(); // update others about this error api.fire( 'error', isObject(error) ? error : { type: 'error', code: 0, body: '' + error, } ); }, // actual processing progress function(computable, current, total) { // update actual duration state.duration = Date.now() - state.timestamp; // update actual progress state.progress = computable ? current / total : null; progressFn(); }, // abort does not expect a value function() { // stop updater state.perceivedPerformanceUpdater.clear(); // fire the abort event so we can switch visuals api.fire('abort', state.response ? state.response.body : null); }, // register the id for this transfer function(transferId) { api.fire('transfer', transferId); } ); }; var abort = function abort() { // no request running, can't abort if (!state.request) return; // stop updater state.perceivedPerformanceUpdater.clear(); // abort actual request if (state.request.abort) state.request.abort(); // if has response object, we've completed the request state.complete = true; }; var reset = function reset() { abort(); state.complete = false; state.perceivedProgress = 0; state.progress = 0; state.timestamp = null; state.perceivedDuration = 0; state.duration = 0; state.request = null; state.response = null; }; var getProgress = allowMinimumUploadDuration ? function() { return state.progress ? Math.min(state.progress, state.perceivedProgress) : null; } : function() { return state.progress || null; }; var getDuration = allowMinimumUploadDuration ? function() { return Math.min(state.duration, state.perceivedDuration); } : function() { return state.duration; }; var api = Object.assign({}, on(), { process: process, // start processing file abort: abort, // abort active process request getProgress: getProgress, getDuration: getDuration, reset: reset, }); return api; }; var getFilenameWithoutExtension = function getFilenameWithoutExtension(name) { return name.substring(0, name.lastIndexOf('.')) || name; }; var createFileStub = function createFileStub(source) { var data = [source.name, source.size, source.type]; // is blob or base64, then we need to set the name if (source instanceof Blob || isBase64DataURI(source)) { data[0] = source.name || getDateString(); } else if (isBase64DataURI(source)) { // if is base64 data uri we need to determine the average size and type data[1] = source.length; data[2] = getMimeTypeFromBase64DataURI(source); } else if (isString(source)) { // url data[0] = getFilenameFromURL(source); data[1] = 0; data[2] = 'application/octet-stream'; } return { name: data[0], size: data[1], type: data[2], }; }; var isFile = function isFile(value) { return !!(value instanceof File || (value instanceof Blob && value.name)); }; var deepCloneObject = function deepCloneObject(src) { if (!isObject(src)) return src; var target = isArray(src) ? [] : {}; for (var key in src) { if (!src.hasOwnProperty(key)) continue; var v = src[key]; target[key] = v && isObject(v) ? deepCloneObject(v) : v; } return target; }; var createItem = function createItem() { var origin = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; var serverFileReference = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; var file = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; // unique id for this item, is used to identify the item across views var id = getUniqueId(); /** * Internal item state */ var state = { // is archived archived: false, // if is frozen, no longer fires events frozen: false, // removed from view released: false, // original source source: null, // file model reference file: file, // id of file on server serverFileReference: serverFileReference, // id of file transfer on server transferId: null, // is aborted processingAborted: false, // current item status status: serverFileReference ? ItemStatus.PROCESSING_COMPLETE : ItemStatus.INIT, // active processes activeLoader: null, activeProcessor: null, }; // callback used when abort processing is called to link back to the resolve method var abortProcessingRequestComplete = null; /** * Externally added item metadata */ var metadata = {}; // item data var setStatus = function setStatus(status) { return (state.status = status); }; // fire event unless the item has been archived var fire = function fire(event) { if (state.released || state.frozen) return; for ( var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++ ) { params[_key - 1] = arguments[_key]; } api.fire.apply(api, [event].concat(params)); }; // file data var getFileExtension = function getFileExtension() { return getExtensionFromFilename(state.file.name); }; var getFileType = function getFileType() { return state.file.type; }; var getFileSize = function getFileSize() { return state.file.size; }; var getFile = function getFile() { return state.file; }; // // logic to load a file // var load = function load(source, loader, onload) { // remember the original item source state.source = source; // source is known api.fireSync('init'); // file stub is already there if (state.file) { api.fireSync('load-skip'); return; } // set a stub file object while loading the actual data state.file = createFileStub(source); // starts loading loader.on('init', function() { fire('load-init'); }); // we'eve received a size indication, let's update the stub loader.on('meta', function(meta) { // set size of file stub state.file.size = meta.size; // set name of file stub state.file.filename = meta.filename; // if has received source, we done if (meta.source) { origin = FileOrigin.LIMBO; state.serverFileReference = meta.source; state.status = ItemStatus.PROCESSING_COMPLETE; } // size has been updated fire('load-meta'); }); // the file is now loading we need to update the progress indicators loader.on('progress', function(progress) { setStatus(ItemStatus.LOADING); fire('load-progress', progress); }); // an error was thrown while loading the file, we need to switch to error state loader.on('error', function(error) { setStatus(ItemStatus.LOAD_ERROR); fire('load-request-error', error); }); // user or another process aborted the file load (cannot retry) loader.on('abort', function() { setStatus(ItemStatus.INIT); fire('load-abort'); }); // done loading loader.on('load', function(file) { // as we've now loaded the file the loader is no longer required state.activeLoader = null; // called when file has loaded succesfully var success = function success(result) { // set (possibly) transformed file state.file = isFile(result) ? result : state.file; // file received if (origin === FileOrigin.LIMBO && state.serverFileReference) { setStatus(ItemStatus.PROCESSING_COMPLETE); } else { setStatus(ItemStatus.IDLE); } fire('load'); }; var error = function error(result) { // set original file state.file = file; fire('load-meta'); setStatus(ItemStatus.LOAD_ERROR); fire('load-file-error', result); }; // if we already have a server file reference, we don't need to call the onload method if (state.serverFileReference) { success(file); return; } // no server id, let's give this file the full treatment onload(file, success, error); }); // set loader source data loader.setSource(source); // set as active loader state.activeLoader = loader; // load the source data loader.load(); }; var retryLoad = function retryLoad() { if (!state.activeLoader) { return; } state.activeLoader.load(); }; var abortLoad = function abortLoad() { if (state.activeLoader) { state.activeLoader.abort(); return; } setStatus(ItemStatus.INIT); fire('load-abort'); }; // // logic to process a file // var process = function process(processor, onprocess) { // processing was aborted if (state.processingAborted) { state.processingAborted = false; return; } // now processing setStatus(ItemStatus.PROCESSING); // reset abort callback abortProcessingRequestComplete = null; // if no file loaded we'll wait for the load event if (!(state.file instanceof Blob)) { api.on('load', function() { process(processor, onprocess); }); return; } // setup processor processor.on('load', function(serverFileReference) { // need this id to be able to revert the upload state.transferId = null; state.serverFileReference = serverFileReference; }); // register transfer id processor.on('transfer', function(transferId) { // need this id to be able to revert the upload state.transferId = transferId; }); processor.on('load-perceived', function(serverFileReference) { // no longer required state.activeProcessor = null; // need this id to be able to rever the upload state.transferId = null; state.serverFileReference = serverFileReference; setStatus(ItemStatus.PROCESSING_COMPLETE); fire('process-complete', serverFileReference); }); processor.on('start', function() { fire('process-start'); }); processor.on('error', function(error) { state.activeProcessor = null; setStatus(ItemStatus.PROCESSING_ERROR); fire('process-error', error); }); processor.on('abort', function(serverFileReference) { state.activeProcessor = null; // if file was uploaded but processing was cancelled during perceived processor time store file reference state.serverFileReference = serverFileReference; setStatus(ItemStatus.IDLE); fire('process-abort'); // has timeout so doesn't interfere with remove action if (abortProcessingRequestComplete) { abortProcessingRequestComplete(); } }); processor.on('progress', function(progress) { fire('process-progress', progress); }); // when successfully transformed var success = function success(file) { // if was archived in the mean time, don't process if (state.archived) return; // process file! processor.process(file, Object.assign({}, metadata)); }; // something went wrong during transform phase var error = console.error; // start processing the file onprocess(state.file, success, error); // set as active processor state.activeProcessor = processor; }; var requestProcessing = function requestProcessing() { state.processingAborted = false; setStatus(ItemStatus.PROCESSING_QUEUED); }; var abortProcessing = function abortProcessing() { return new Promise(function(resolve) { if (!state.activeProcessor) { state.processingAborted = true; setStatus(ItemStatus.IDLE); fire('process-abort'); resolve(); return; } abortProcessingRequestComplete = function abortProcessingRequestComplete() { resolve(); }; state.activeProcessor.abort(); }); }; // // logic to revert a processed file // var revert = function revert(revertFileUpload, forceRevert) { return new Promise(function(resolve, reject) { // a completed upload will have a serverFileReference, a failed chunked upload where // getting a serverId succeeded but >=0 chunks have been uploaded will have transferId set var serverTransferId = state.serverFileReference !== null ? state.serverFileReference : state.transferId; // cannot revert without a server id for this process if (serverTransferId === null) { resolve(); return; } // revert the upload (fire and forget) revertFileUpload( serverTransferId, function() { // reset file server id and transfer id as now it's not available on the server state.serverFileReference = null; state.transferId = null; resolve(); }, function(error) { // don't set error state when reverting is optional, it will always resolve if (!forceRevert) { resolve(); return; } // oh no errors setStatus(ItemStatus.PROCESSING_REVERT_ERROR); fire('process-revert-error'); reject(error); } ); // fire event setStatus(ItemStatus.IDLE); fire('process-revert'); }); }; // exposed methods var _setMetadata = function setMetadata(key, value, silent) { var keys = key.split('.'); var root = keys[0]; var last = keys.pop(); var data = metadata; keys.forEach(function(key) { return (data = data[key]); }); // compare old value against new value, if they're the same, we're not updating if (JSON.stringify(data[last]) === JSON.stringify(value)) return; // update value data[last] = value; // fire update fire('metadata-update', { key: root, value: metadata[root], silent: silent, }); }; var getMetadata = function getMetadata(key) { return deepCloneObject(key ? metadata[key] : metadata); }; var api = Object.assign( { id: { get: function get() { return id; }, }, origin: { get: function get() { return origin; }, set: function set(value) { return (origin = value); }, }, serverId: { get: function get() { return state.serverFileReference; }, }, transferId: { get: function get() { return state.transferId; }, }, status: { get: function get() { return state.status; }, }, filename: { get: function get() { return state.file.name; }, }, filenameWithoutExtension: { get: function get() { return getFilenameWithoutExtension(state.file.name); }, }, fileExtension: { get: getFileExtension }, fileType: { get: getFileType }, fileSize: { get: getFileSize }, file: { get: getFile }, relativePath: { get: function get() { return state.file._relativePath; }, }, source: { get: function get() { return state.source; }, }, getMetadata: getMetadata, setMetadata: function setMetadata(key, value, silent) { if (isObject(key)) { var data = key; Object.keys(data).forEach(function(key) { _setMetadata(key, data[key], value); }); return key; } _setMetadata(key, value, silent); return value; }, extend: function extend(name, handler) { return (itemAPI[name] = handler); }, abortLoad: abortLoad, retryLoad: retryLoad, requestProcessing: requestProcessing, abortProcessing: abortProcessing, load: load, process: process, revert: revert, }, on(), { freeze: function freeze() { return (state.frozen = true); }, release: function release() { return (state.released = true); }, released: { get: function get() { return state.released; }, }, archive: function archive() { return (state.archived = true); }, archived: { get: function get() { return state.archived; }, }, // replace source and file object setFile: function setFile(file) { return (state.file = file); }, } ); // create it here instead of returning it instantly so we can extend it later var itemAPI = createObject(api); return itemAPI; }; var getItemIndexByQuery = function getItemIndexByQuery(items, query) { // just return first index if (isEmpty(query)) { return 0; } // invalid queries if (!isString(query)) { return -1; } // return item by id (or -1 if not found) return items.findIndex(function(item) { return item.id === query; }); }; var getItemById = function getItemById(items, itemId) { var index = getItemIndexByQuery(items, itemId); if (index < 0) { return; } return items[index] || null; }; var fetchBlob = function fetchBlob(url, load, error, progress, abort, headers) { var request = sendRequest(null, url, { method: 'GET', responseType: 'blob', }); request.onload = function(xhr) { // get headers var headers = xhr.getAllResponseHeaders(); // get filename var filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url); // create response load( createResponse('load', xhr.status, getFileFromBlob(xhr.response, filename), headers) ); }; request.onerror = function(xhr) { error(createResponse('error', xhr.status, xhr.statusText, xhr.getAllResponseHeaders())); }; request.onheaders = function(xhr) { headers(createResponse('headers', xhr.status, null, xhr.getAllResponseHeaders())); }; request.ontimeout = createTimeoutResponse(error); request.onprogress = progress; request.onabort = abort; // should return request return request; }; var getDomainFromURL = function getDomainFromURL(url) { if (url.indexOf('//') === 0) { url = location.protocol + url; } return url .toLowerCase() .replace('blob:', '') .replace(/([a-z])?:\/\//, '$1') .split('/')[0]; }; var isExternalURL = function isExternalURL(url) { return ( (url.indexOf(':') > -1 || url.indexOf('//') > -1) && getDomainFromURL(location.href) !== getDomainFromURL(url) ); }; var dynamicLabel = function dynamicLabel(label) { return function() { return isFunction(label) ? label.apply(void 0, arguments) : label; }; }; var isMockItem = function isMockItem(item) { return !isFile(item.file); }; var listUpdated = function listUpdated(dispatch, state) { clearTimeout(state.listUpdateTimeout); state.listUpdateTimeout = setTimeout(function() { dispatch('DID_UPDATE_ITEMS', { items: getActiveItems(state.items) }); }, 0); }; var optionalPromise = function optionalPromise(fn) { for ( var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++ ) { params[_key - 1] = arguments[_key]; } return new Promise(function(resolve) { if (!fn) { return resolve(true); } var result = fn.apply(void 0, params); if (result == null) { return resolve(true); } if (typeof result === 'boolean') { return resolve(result); } if (typeof result.then === 'function') { result.then(resolve); } }); }; var sortItems = function sortItems(state, compare) { state.items.sort(function(a, b) { return compare(createItemAPI(a), createItemAPI(b)); }); }; // returns item based on state var getItemByQueryFromState = function getItemByQueryFromState(state, itemHandler) { return function() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var query = _ref.query, _ref$success = _ref.success, success = _ref$success === void 0 ? function() {} : _ref$success, _ref$failure = _ref.failure, failure = _ref$failure === void 0 ? function() {} : _ref$failure, options = _objectWithoutProperties(_ref, ['query', 'success', 'failure']); var item = getItemByQuery(state.items, query); if (!item) { failure({ error: createResponse('error', 0, 'Item not found'), file: null, }); return; } itemHandler(item, success, failure, options || {}); }; }; var actions = function actions(dispatch, query, state) { return { /** * Aborts all ongoing processes */ ABORT_ALL: function ABORT_ALL() { getActiveItems(state.items).forEach(function(item) { item.freeze(); item.abortLoad(); item.abortProcessing(); }); }, /** * Sets initial files */ DID_SET_FILES: function DID_SET_FILES(_ref2) { var _ref2$value = _ref2.value, value = _ref2$value === void 0 ? [] : _ref2$value; // map values to file objects var files = value.map(function(file) { return { source: file.source ? file.source : file, options: file.options, }; }); // loop over files, if file is in list, leave it be, if not, remove // test if items should be moved var activeItems = getActiveItems(state.items); activeItems.forEach(function(item) { // if item not is in new value, remove if ( !files.find(function(file) { return file.source === item.source || file.source === item.file; }) ) { dispatch('REMOVE_ITEM', { query: item, remove: false }); } }); // add new files activeItems = getActiveItems(state.items); files.forEach(function(file, index) { // if file is already in list if ( activeItems.find(function(item) { return item.source === file.source || item.file === file.source; }) ) return; // not in list, add dispatch( 'ADD_ITEM', Object.assign({}, file, { interactionMethod: InteractionMethod.NONE, index: index, }) ); }); }, DID_UPDATE_ITEM_METADATA: function DID_UPDATE_ITEM_METADATA(_ref3) { var id = _ref3.id, action = _ref3.action, change = _ref3.change; // don't do anything if (change.silent) return; // if is called multiple times in close succession we combined all calls together to save resources clearTimeout(state.itemUpdateTimeout); state.itemUpdateTimeout = setTimeout(function() { var item = getItemById(state.items, id); // only revert and attempt to upload when we're uploading to a server if (!query('IS_ASYNC')) { // should we update the output data applyFilterChain('SHOULD_PREPARE_OUTPUT', false, { item: item, query: query, action: action, change: change, }).then(function(shouldPrepareOutput) { // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook var beforePrepareFile = query('GET_BEFORE_PREPARE_FILE'); if (beforePrepareFile) shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput); if (!shouldPrepareOutput) return; dispatch( 'REQUEST_PREPARE_OUTPUT', { query: id, item: item, success: function success(file) { dispatch('DID_PREPARE_OUTPUT', { id: id, file: file }); }, }, true ); }); return; } // if is local item we need to enable upload button so change can be propagated to server if (item.origin === FileOrigin.LOCAL) { dispatch('DID_LOAD_ITEM', { id: item.id, error: null, serverFileReference: item.source, }); } // for async scenarios var upload = function upload() { // we push this forward a bit so the interface is updated correctly setTimeout(function() { dispatch('REQUEST_ITEM_PROCESSING', { query: id }); }, 32); }; var revert = function revert(doUpload) { item.revert( createRevertFunction( state.options.server.url, state.options.server.revert ), query('GET_FORCE_REVERT') ) .then(doUpload ? upload : function() {}) .catch(function() {}); }; var abort = function abort(doUpload) { item.abortProcessing().then(doUpload ? upload : function() {}); }; // if we should re-upload the file immediately if (item.status === ItemStatus.PROCESSING_COMPLETE) { return revert(state.options.instantUpload); } // if currently uploading, cancel upload if (item.status === ItemStatus.PROCESSING) { return abort(state.options.instantUpload); } if (state.options.instantUpload) { upload(); } }, 0); }, MOVE_ITEM: function MOVE_ITEM(_ref4) { var query = _ref4.query, index = _ref4.index; var item = getItemByQuery(state.items, query); if (!item) return; var currentIndex = state.items.indexOf(item); index = limit(index, 0, state.items.length - 1); if (currentIndex === index) return; state.items.splice(index, 0, state.items.splice(currentIndex, 1)[0]); }, SORT: function SORT(_ref5) { var compare = _ref5.compare; sortItems(state, compare); dispatch('DID_SORT_ITEMS', { items: query('GET_ACTIVE_ITEMS'), }); }, ADD_ITEMS: function ADD_ITEMS(_ref6) { var items = _ref6.items, index = _ref6.index, interactionMethod = _ref6.interactionMethod, _ref6$success = _ref6.success, success = _ref6$success === void 0 ? function() {} : _ref6$success, _ref6$failure = _ref6.failure, failure = _ref6$failure === void 0 ? function() {} : _ref6$failure; var currentIndex = index; if (index === -1 || typeof index === 'undefined') { var insertLocation = query('GET_ITEM_INSERT_LOCATION'); var totalItems = query('GET_TOTAL_ITEMS'); currentIndex = insertLocation === 'before' ? 0 : totalItems; } var ignoredFiles = query('GET_IGNORED_FILES'); var isValidFile = function isValidFile(source) { return isFile(source) ? !ignoredFiles.includes(source.name.toLowerCase()) : !isEmpty(source); }; var validItems = items.filter(isValidFile); var promises = validItems.map(function(source) { return new Promise(function(resolve, reject) { dispatch('ADD_ITEM', { interactionMethod: interactionMethod, source: source.source || source, success: resolve, failure: reject, index: currentIndex++, options: source.options || {}, }); }); }); Promise.all(promises) .then(success) .catch(failure); }, /** * @param source * @param index * @param interactionMethod */ ADD_ITEM: function ADD_ITEM(_ref7) { var source = _ref7.source, _ref7$index = _ref7.index, index = _ref7$index === void 0 ? -1 : _ref7$index, interactionMethod = _ref7.interactionMethod, _ref7$success = _ref7.success, success = _ref7$success === void 0 ? function() {} : _ref7$success, _ref7$failure = _ref7.failure, failure = _ref7$failure === void 0 ? function() {} : _ref7$failure, _ref7$options = _ref7.options, options = _ref7$options === void 0 ? {} : _ref7$options; // if no source supplied if (isEmpty(source)) { failure({ error: createResponse('error', 0, 'No source'), file: null, }); return; } // filter out invalid file items, used to filter dropped directory contents if ( isFile(source) && state.options.ignoredFiles.includes(source.name.toLowerCase()) ) { // fail silently return; } // test if there's still room in the list of files if (!hasRoomForItem(state)) { // if multiple allowed, we can't replace // or if only a single item is allowed but we're not allowed to replace it we exit if ( state.options.allowMultiple || (!state.options.allowMultiple && !state.options.allowReplace) ) { var error = createResponse('warning', 0, 'Max files'); dispatch('DID_THROW_MAX_FILES', { source: source, error: error, }); failure({ error: error, file: null }); return; } // let's replace the item // id of first item we're about to remove var _item = getActiveItems(state.items)[0]; // if has been processed remove it from the server as well if ( _item.status === ItemStatus.PROCESSING_COMPLETE || _item.status === ItemStatus.PROCESSING_REVERT_ERROR ) { var forceRevert = query('GET_FORCE_REVERT'); _item .revert( createRevertFunction( state.options.server.url, state.options.server.revert ), forceRevert ) .then(function() { if (!forceRevert) return; // try to add now dispatch('ADD_ITEM', { source: source, index: index, interactionMethod: interactionMethod, success: success, failure: failure, options: options, }); }) .catch(function() {}); // no need to handle this catch state for now if (forceRevert) return; } // remove first item as it will be replaced by this item dispatch('REMOVE_ITEM', { query: _item.id }); } // where did the file originate var origin = options.type === 'local' ? FileOrigin.LOCAL : options.type === 'limbo' ? FileOrigin.LIMBO : FileOrigin.INPUT; // create a new blank item var item = createItem( // where did this file come from origin, // an input file never has a server file reference origin === FileOrigin.INPUT ? null : source, // file mock data, if defined options.file ); // set initial meta data Object.keys(options.metadata || {}).forEach(function(key) { item.setMetadata(key, options.metadata[key]); }); // created the item, let plugins add methods applyFilters('DID_CREATE_ITEM', item, { query: query, dispatch: dispatch }); // where to insert new items var itemInsertLocation = query('GET_ITEM_INSERT_LOCATION'); // adjust index if is not allowed to pick location if (!state.options.itemInsertLocationFreedom) { index = itemInsertLocation === 'before' ? -1 : state.items.length; } // add item to list insertItem(state.items, item, index); // sort items in list if (isFunction(itemInsertLocation) && source) { sortItems(state, itemInsertLocation); } // get a quick reference to the item id var id = item.id; // observe item events item.on('init', function() { dispatch('DID_INIT_ITEM', { id: id }); }); item.on('load-init', function() { dispatch('DID_START_ITEM_LOAD', { id: id }); }); item.on('load-meta', function() { dispatch('DID_UPDATE_ITEM_META', { id: id }); }); item.on('load-progress', function(progress) { dispatch('DID_UPDATE_ITEM_LOAD_PROGRESS', { id: id, progress: progress }); }); item.on('load-request-error', function(error) { var mainStatus = dynamicLabel(state.options.labelFileLoadError)(error); // is client error, no way to recover if (error.code >= 400 && error.code < 500) { dispatch('DID_THROW_ITEM_INVALID', { id: id, error: error, status: { main: mainStatus, sub: error.code + ' (' + error.body + ')', }, }); // reject the file so can be dealt with through API failure({ error: error, file: createItemAPI(item) }); return; } // is possible server error, so might be possible to retry dispatch('DID_THROW_ITEM_LOAD_ERROR', { id: id, error: error, status: { main: mainStatus, sub: state.options.labelTapToRetry, }, }); }); item.on('load-file-error', function(error) { dispatch('DID_THROW_ITEM_INVALID', { id: id, error: error.status, status: error.status, }); failure({ error: error.status, file: createItemAPI(item) }); }); item.on('load-abort', function() { dispatch('REMOVE_ITEM', { query: id }); }); item.on('load-skip', function() { item.on('metadata-update', function(change) { if (!isFile(item.file)) return; dispatch('DID_UPDATE_ITEM_METADATA', { id: id, change: change }); }); dispatch('COMPLETE_LOAD_ITEM', { query: id, item: item, data: { source: source, success: success, }, }); }); item.on('load', function() { var handleAdd = function handleAdd(shouldAdd) { // no should not add this file if (!shouldAdd) { dispatch('REMOVE_ITEM', { query: id, }); return; } // now interested in metadata updates item.on('metadata-update', function(change) { dispatch('DID_UPDATE_ITEM_METADATA', { id: id, change: change }); }); // let plugins decide if the output data should be prepared at this point // means we'll do this and wait for idle state applyFilterChain('SHOULD_PREPARE_OUTPUT', false, { item: item, query: query, }).then(function(shouldPrepareOutput) { // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook var beforePrepareFile = query('GET_BEFORE_PREPARE_FILE'); if (beforePrepareFile) shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput); var loadComplete = function loadComplete() { dispatch('COMPLETE_LOAD_ITEM', { query: id, item: item, data: { source: source, success: success, }, }); listUpdated(dispatch, state); }; // exit if (shouldPrepareOutput) { // wait for idle state and then run PREPARE_OUTPUT dispatch( 'REQUEST_PREPARE_OUTPUT', { query: id, item: item, success: function success(file) { dispatch('DID_PREPARE_OUTPUT', { id: id, file: file }); loadComplete(); }, }, true ); return; } loadComplete(); }); }; // item loaded, allow plugins to // - read data (quickly) // - add metadata applyFilterChain('DID_LOAD_ITEM', item, { query: query, dispatch: dispatch }) .then(function() { optionalPromise(query('GET_BEFORE_ADD_FILE'), createItemAPI(item)).then( handleAdd ); }) .catch(function(e) { if (!e || !e.error || !e.status) return handleAdd(false); dispatch('DID_THROW_ITEM_INVALID', { id: id, error: e.error, status: e.status, }); }); }); item.on('process-start', function() { dispatch('DID_START_ITEM_PROCESSING', { id: id }); }); item.on('process-progress', function(progress) { dispatch('DID_UPDATE_ITEM_PROCESS_PROGRESS', { id: id, progress: progress }); }); item.on('process-error', function(error) { dispatch('DID_THROW_ITEM_PROCESSING_ERROR', { id: id, error: error, status: { main: dynamicLabel(state.options.labelFileProcessingError)(error), sub: state.options.labelTapToRetry, }, }); }); item.on('process-revert-error', function(error) { dispatch('DID_THROW_ITEM_PROCESSING_REVERT_ERROR', { id: id, error: error, status: { main: dynamicLabel(state.options.labelFileProcessingRevertError)(error), sub: state.options.labelTapToRetry, }, }); }); item.on('process-complete', function(serverFileReference) { dispatch('DID_COMPLETE_ITEM_PROCESSING', { id: id, error: null, serverFileReference: serverFileReference, }); dispatch('DID_DEFINE_VALUE', { id: id, value: serverFileReference }); }); item.on('process-abort', function() { dispatch('DID_ABORT_ITEM_PROCESSING', { id: id }); }); item.on('process-revert', function() { dispatch('DID_REVERT_ITEM_PROCESSING', { id: id }); dispatch('DID_DEFINE_VALUE', { id: id, value: null }); }); // let view know the item has been inserted dispatch('DID_ADD_ITEM', { id: id, index: index, interactionMethod: interactionMethod, }); listUpdated(dispatch, state); // start loading the source var _ref8 = state.options.server || {}, url = _ref8.url, load = _ref8.load, restore = _ref8.restore, fetch = _ref8.fetch; item.load( source, // this creates a function that loads the file based on the type of file (string, base64, blob, file) and location of file (local, remote, limbo) createFileLoader( origin === FileOrigin.INPUT ? // input, if is remote, see if should use custom fetch, else use default fetchBlob isString(source) && isExternalURL(source) ? fetch ? createFetchFunction(url, fetch) : fetchBlob // remote url : fetchBlob // try to fetch url : // limbo or local origin === FileOrigin.LIMBO ? createFetchFunction(url, restore) // limbo : createFetchFunction(url, load) // local ), // called when the file is loaded so it can be piped through the filters function(file, success, error) { // let's process the file applyFilterChain('LOAD_FILE', file, { query: query }) .then(success) .catch(error); } ); }, REQUEST_PREPARE_OUTPUT: function REQUEST_PREPARE_OUTPUT(_ref9) { var item = _ref9.item, success = _ref9.success, _ref9$failure = _ref9.failure, failure = _ref9$failure === void 0 ? function() {} : _ref9$failure; // error response if item archived var err = { error: createResponse('error', 0, 'Item not found'), file: null, }; // don't handle archived items, an item could have been archived (load aborted) while waiting to be prepared if (item.archived) return failure(err); // allow plugins to alter the file data applyFilterChain('PREPARE_OUTPUT', item.file, { query: query, item: item }).then( function(result) { applyFilterChain('COMPLETE_PREPARE_OUTPUT', result, { query: query, item: item, }).then(function(result) { // don't handle archived items, an item could have been archived (load aborted) while being prepared if (item.archived) return failure(err); // we done! success(result); }); } ); }, COMPLETE_LOAD_ITEM: function COMPLETE_LOAD_ITEM(_ref10) { var item = _ref10.item, data = _ref10.data; var success = data.success, source = data.source; // sort items in list var itemInsertLocation = query('GET_ITEM_INSERT_LOCATION'); if (isFunction(itemInsertLocation) && source) { sortItems(state, itemInsertLocation); } // let interface know the item has loaded dispatch('DID_LOAD_ITEM', { id: item.id, error: null, serverFileReference: item.origin === FileOrigin.INPUT ? null : source, }); // item has been successfully loaded and added to the // list of items so can now be safely returned for use success(createItemAPI(item)); // if this is a local server file we need to show a different state if (item.origin === FileOrigin.LOCAL) { dispatch('DID_LOAD_LOCAL_ITEM', { id: item.id }); return; } // if is a temp server file we prevent async upload call here (as the file is already on the server) if (item.origin === FileOrigin.LIMBO) { dispatch('DID_COMPLETE_ITEM_PROCESSING', { id: item.id, error: null, serverFileReference: source, }); dispatch('DID_DEFINE_VALUE', { id: item.id, value: item.serverId || source, }); return; } // id we are allowed to upload the file immediately, lets do it if (query('IS_ASYNC') && state.options.instantUpload) { dispatch('REQUEST_ITEM_PROCESSING', { query: item.id }); } }, RETRY_ITEM_LOAD: getItemByQueryFromState(state, function(item) { // try loading the source one more time item.retryLoad(); }), REQUEST_ITEM_PREPARE: getItemByQueryFromState(state, function(item, _success, failure) { dispatch( 'REQUEST_PREPARE_OUTPUT', { query: item.id, item: item, success: function success(file) { dispatch('DID_PREPARE_OUTPUT', { id: item.id, file: file }); _success({ file: item, output: file, }); }, failure: failure, }, true ); }), REQUEST_ITEM_PROCESSING: getItemByQueryFromState(state, function( item, success, failure ) { // cannot be queued (or is already queued) var itemCanBeQueuedForProcessing = // waiting for something item.status === ItemStatus.IDLE || // processing went wrong earlier item.status === ItemStatus.PROCESSING_ERROR; // not ready to be processed if (!itemCanBeQueuedForProcessing) { var processNow = function processNow() { return dispatch('REQUEST_ITEM_PROCESSING', { query: item, success: success, failure: failure, }); }; var process = function process() { return document.hidden ? processNow() : setTimeout(processNow, 32); }; // if already done processing or tried to revert but didn't work, try again if ( item.status === ItemStatus.PROCESSING_COMPLETE || item.status === ItemStatus.PROCESSING_REVERT_ERROR ) { item.revert( createRevertFunction( state.options.server.url, state.options.server.revert ), query('GET_FORCE_REVERT') ) .then(process) .catch(function() {}); // don't continue with processing if something went wrong } else if (item.status === ItemStatus.PROCESSING) { item.abortProcessing().then(process); } return; } // already queued for processing if (item.status === ItemStatus.PROCESSING_QUEUED) return; item.requestProcessing(); dispatch('DID_REQUEST_ITEM_PROCESSING', { id: item.id }); dispatch('PROCESS_ITEM', { query: item, success: success, failure: failure }, true); }), PROCESS_ITEM: getItemByQueryFromState(state, function(item, success, failure) { var maxParallelUploads = query('GET_MAX_PARALLEL_UPLOADS'); var totalCurrentUploads = query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING) .length; // queue and wait till queue is freed up if (totalCurrentUploads === maxParallelUploads) { // queue for later processing state.processingQueue.push({ id: item.id, success: success, failure: failure, }); // stop it! return; } // if was not queued or is already processing exit here if (item.status === ItemStatus.PROCESSING) return; var processNext = function processNext() { // process queueud items var queueEntry = state.processingQueue.shift(); // no items left if (!queueEntry) return; // get item reference var id = queueEntry.id, success = queueEntry.success, failure = queueEntry.failure; var itemReference = getItemByQuery(state.items, id); // if item was archived while in queue, jump to next if (!itemReference || itemReference.archived) { processNext(); return; } // process queued item dispatch( 'PROCESS_ITEM', { query: id, success: success, failure: failure }, true ); }; // we done function item.onOnce('process-complete', function() { success(createItemAPI(item)); processNext(); // if origin is local, and we're instant uploading, trigger remove of original // as revert will remove file from list var server = state.options.server; var instantUpload = state.options.instantUpload; if ( instantUpload && item.origin === FileOrigin.LOCAL && isFunction(server.remove) ) { var noop = function noop() {}; item.origin = FileOrigin.LIMBO; state.options.server.remove(item.source, noop, noop); } // All items processed? No errors? var allItemsProcessed = query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING_COMPLETE).length === state.items.length; if (allItemsProcessed) { dispatch('DID_COMPLETE_ITEM_PROCESSING_ALL'); } }); // we error function item.onOnce('process-error', function(error) { failure({ error: error, file: createItemAPI(item) }); processNext(); }); // abort function item.onOnce('process-abort', function() { processNext(); }); // start file processing var options = state.options; item.process( createFileProcessor( createProcessorFunction( options.server.url, options.server.process, options.name, { chunkTransferId: item.transferId, chunkServer: options.server.patch, chunkUploads: options.chunkUploads, chunkForce: options.chunkForce, chunkSize: options.chunkSize, chunkRetryDelays: options.chunkRetryDelays, } ), { allowMinimumUploadDuration: query('GET_ALLOW_MINIMUM_UPLOAD_DURATION'), } ), // called when the file is about to be processed so it can be piped through the transform filters function(file, success, error) { // allow plugins to alter the file data applyFilterChain('PREPARE_OUTPUT', file, { query: query, item: item }) .then(function(file) { dispatch('DID_PREPARE_OUTPUT', { id: item.id, file: file }); success(file); }) .catch(error); } ); }), RETRY_ITEM_PROCESSING: getItemByQueryFromState(state, function(item) { dispatch('REQUEST_ITEM_PROCESSING', { query: item }); }), REQUEST_REMOVE_ITEM: getItemByQueryFromState(state, function(item) { optionalPromise(query('GET_BEFORE_REMOVE_FILE'), createItemAPI(item)).then(function( shouldRemove ) { if (!shouldRemove) { return; } dispatch('REMOVE_ITEM', { query: item }); }); }), RELEASE_ITEM: getItemByQueryFromState(state, function(item) { item.release(); }), REMOVE_ITEM: getItemByQueryFromState(state, function(item, success, failure, options) { var removeFromView = function removeFromView() { // get id reference var id = item.id; // archive the item, this does not remove it from the list getItemById(state.items, id).archive(); // tell the view the item has been removed dispatch('DID_REMOVE_ITEM', { error: null, id: id, item: item }); // now the list has been modified listUpdated(dispatch, state); // correctly removed success(createItemAPI(item)); }; // if this is a local file and the `server.remove` function has been configured, // send source there so dev can remove file from server var server = state.options.server; if ( item.origin === FileOrigin.LOCAL && server && isFunction(server.remove) && options.remove !== false ) { dispatch('DID_START_ITEM_REMOVE', { id: item.id }); server.remove( item.source, function() { return removeFromView(); }, function(status) { dispatch('DID_THROW_ITEM_REMOVE_ERROR', { id: item.id, error: createResponse('error', 0, status, null), status: { main: dynamicLabel(state.options.labelFileRemoveError)(status), sub: state.options.labelTapToRetry, }, }); } ); } else { // if is requesting revert and can revert need to call revert handler (not calling request_ because that would also trigger beforeRemoveHook) if ( (options.revert && item.origin !== FileOrigin.LOCAL && item.serverId !== null) || // if chunked uploads are enabled and we're uploading in chunks for this specific file // or if the file isn't big enough for chunked uploads but chunkForce is set then call // revert before removing from the view... (state.options.chunkUploads && item.file.size > state.options.chunkSize) || (state.options.chunkUploads && state.options.chunkForce) ) { item.revert( createRevertFunction( state.options.server.url, state.options.server.revert ), query('GET_FORCE_REVERT') ); } // can now safely remove from view removeFromView(); } }), ABORT_ITEM_LOAD: getItemByQueryFromState(state, function(item) { item.abortLoad(); }), ABORT_ITEM_PROCESSING: getItemByQueryFromState(state, function(item) { // test if is already processed if (item.serverId) { dispatch('REVERT_ITEM_PROCESSING', { id: item.id }); return; } // abort item.abortProcessing().then(function() { var shouldRemove = state.options.instantUpload; if (shouldRemove) { dispatch('REMOVE_ITEM', { query: item.id }); } }); }), REQUEST_REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, function(item) { // not instant uploading, revert immediately if (!state.options.instantUpload) { dispatch('REVERT_ITEM_PROCESSING', { query: item }); return; } // if we're instant uploading the file will also be removed if we revert, // so if a before remove file hook is defined we need to run it now var handleRevert = function handleRevert(shouldRevert) { if (!shouldRevert) return; dispatch('REVERT_ITEM_PROCESSING', { query: item }); }; var fn = query('GET_BEFORE_REMOVE_FILE'); if (!fn) { return handleRevert(true); } var requestRemoveResult = fn(createItemAPI(item)); if (requestRemoveResult == null) { // undefined or null return handleRevert(true); } if (typeof requestRemoveResult === 'boolean') { return handleRevert(requestRemoveResult); } if (typeof requestRemoveResult.then === 'function') { requestRemoveResult.then(handleRevert); } }), REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, function(item) { item.revert( createRevertFunction(state.options.server.url, state.options.server.revert), query('GET_FORCE_REVERT') ) .then(function() { var shouldRemove = state.options.instantUpload || isMockItem(item); if (shouldRemove) { dispatch('REMOVE_ITEM', { query: item.id }); } }) .catch(function() {}); }), SET_OPTIONS: function SET_OPTIONS(_ref11) { var options = _ref11.options; // get all keys passed var optionKeys = Object.keys(options); // get prioritized keyed to include (remove once not in options object) var prioritizedOptionKeys = PrioritizedOptions.filter(function(key) { return optionKeys.includes(key); }); // order the keys, prioritized first, then rest var orderedOptionKeys = [].concat( _toConsumableArray(prioritizedOptionKeys), _toConsumableArray( Object.keys(options).filter(function(key) { return !prioritizedOptionKeys.includes(key); }) ) ); // dispatch set event for each option orderedOptionKeys.forEach(function(key) { dispatch('SET_' + fromCamels(key, '_').toUpperCase(), { value: options[key], }); }); }, }; }; var PrioritizedOptions = ['server']; var formatFilename = function formatFilename(name) { return name; }; var createElement$1 = function createElement(tagName) { return document.createElement(tagName); }; var text = function text(node, value) { var textNode = node.childNodes[0]; if (!textNode) { textNode = document.createTextNode(value); node.appendChild(textNode); } else if (value !== textNode.nodeValue) { textNode.nodeValue = value; } }; var polarToCartesian = function polarToCartesian(centerX, centerY, radius, angleInDegrees) { var angleInRadians = (((angleInDegrees % 360) - 90) * Math.PI) / 180.0; return { x: centerX + radius * Math.cos(angleInRadians), y: centerY + radius * Math.sin(angleInRadians), }; }; var describeArc = function describeArc(x, y, radius, startAngle, endAngle, arcSweep) { var start = polarToCartesian(x, y, radius, endAngle); var end = polarToCartesian(x, y, radius, startAngle); return ['M', start.x, start.y, 'A', radius, radius, 0, arcSweep, 0, end.x, end.y].join(' '); }; var percentageArc = function percentageArc(x, y, radius, from, to) { var arcSweep = 1; if (to > from && to - from <= 0.5) { arcSweep = 0; } if (from > to && from - to >= 0.5) { arcSweep = 0; } return describeArc( x, y, radius, Math.min(0.9999, from) * 360, Math.min(0.9999, to) * 360, arcSweep ); }; var create = function create(_ref) { var root = _ref.root, props = _ref.props; // start at 0 props.spin = false; props.progress = 0; props.opacity = 0; // svg var svg = createElement('svg'); root.ref.path = createElement('path', { 'stroke-width': 2, 'stroke-linecap': 'round', }); svg.appendChild(root.ref.path); root.ref.svg = svg; root.appendChild(svg); }; var write = function write(_ref2) { var root = _ref2.root, props = _ref2.props; if (props.opacity === 0) { return; } if (props.align) { root.element.dataset.align = props.align; } // get width of stroke var ringStrokeWidth = parseInt(attr(root.ref.path, 'stroke-width'), 10); // calculate size of ring var size = root.rect.element.width * 0.5; // ring state var ringFrom = 0; var ringTo = 0; // now in busy mode if (props.spin) { ringFrom = 0; ringTo = 0.5; } else { ringFrom = 0; ringTo = props.progress; } // get arc path var coordinates = percentageArc(size, size, size - ringStrokeWidth, ringFrom, ringTo); // update progress bar attr(root.ref.path, 'd', coordinates); // hide while contains 0 value attr(root.ref.path, 'stroke-opacity', props.spin || props.progress > 0 ? 1 : 0); }; var progressIndicator = createView({ tag: 'div', name: 'progress-indicator', ignoreRectUpdate: true, ignoreRect: true, create: create, write: write, mixins: { apis: ['progress', 'spin', 'align'], styles: ['opacity'], animations: { opacity: { type: 'tween', duration: 500 }, progress: { type: 'spring', stiffness: 0.95, damping: 0.65, mass: 10, }, }, }, }); var create$1 = function create(_ref) { var root = _ref.root, props = _ref.props; root.element.innerHTML = (props.icon || '') + ('' + props.label + ''); props.isDisabled = false; }; var write$1 = function write(_ref2) { var root = _ref2.root, props = _ref2.props; var isDisabled = props.isDisabled; var shouldDisable = root.query('GET_DISABLED') || props.opacity === 0; if (shouldDisable && !isDisabled) { props.isDisabled = true; attr(root.element, 'disabled', 'disabled'); } else if (!shouldDisable && isDisabled) { props.isDisabled = false; root.element.removeAttribute('disabled'); } }; var fileActionButton = createView({ tag: 'button', attributes: { type: 'button', }, ignoreRect: true, ignoreRectUpdate: true, name: 'file-action-button', mixins: { apis: ['label'], styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'], animations: { scaleX: 'spring', scaleY: 'spring', translateX: 'spring', translateY: 'spring', opacity: { type: 'tween', duration: 250 }, }, listeners: true, }, create: create$1, write: write$1, }); var toNaturalFileSize = function toNaturalFileSize(bytes) { var decimalSeparator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '.'; var base = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1000; var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; var _options$labelBytes = options.labelBytes, labelBytes = _options$labelBytes === void 0 ? 'bytes' : _options$labelBytes, _options$labelKilobyt = options.labelKilobytes, labelKilobytes = _options$labelKilobyt === void 0 ? 'KB' : _options$labelKilobyt, _options$labelMegabyt = options.labelMegabytes, labelMegabytes = _options$labelMegabyt === void 0 ? 'MB' : _options$labelMegabyt, _options$labelGigabyt = options.labelGigabytes, labelGigabytes = _options$labelGigabyt === void 0 ? 'GB' : _options$labelGigabyt; // no negative byte sizes bytes = Math.round(Math.abs(bytes)); var KB = base; var MB = base * base; var GB = base * base * base; // just bytes if (bytes < KB) { return bytes + ' ' + labelBytes; } // kilobytes if (bytes < MB) { return Math.floor(bytes / KB) + ' ' + labelKilobytes; } // megabytes if (bytes < GB) { return removeDecimalsWhenZero(bytes / MB, 1, decimalSeparator) + ' ' + labelMegabytes; } // gigabytes return removeDecimalsWhenZero(bytes / GB, 2, decimalSeparator) + ' ' + labelGigabytes; }; var removeDecimalsWhenZero = function removeDecimalsWhenZero(value, decimalCount, separator) { return value .toFixed(decimalCount) .split('.') .filter(function(part) { return part !== '0'; }) .join(separator); }; var create$2 = function create(_ref) { var root = _ref.root, props = _ref.props; // filename var fileName = createElement$1('span'); fileName.className = 'filepond--file-info-main'; // hide for screenreaders // the file is contained in a fieldset with legend that contains the filename // no need to read it twice attr(fileName, 'aria-hidden', 'true'); root.appendChild(fileName); root.ref.fileName = fileName; // filesize var fileSize = createElement$1('span'); fileSize.className = 'filepond--file-info-sub'; root.appendChild(fileSize); root.ref.fileSize = fileSize; // set initial values text(fileSize, root.query('GET_LABEL_FILE_WAITING_FOR_SIZE')); text(fileName, formatFilename(root.query('GET_ITEM_NAME', props.id))); }; var updateFile = function updateFile(_ref2) { var root = _ref2.root, props = _ref2.props; text( root.ref.fileSize, toNaturalFileSize( root.query('GET_ITEM_SIZE', props.id), '.', root.query('GET_FILE_SIZE_BASE'), root.query('GET_FILE_SIZE_LABELS', root.query) ) ); text(root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id))); }; var updateFileSizeOnError = function updateFileSizeOnError(_ref3) { var root = _ref3.root, props = _ref3.props; // if size is available don't fallback to unknown size message if (isInt(root.query('GET_ITEM_SIZE', props.id))) { updateFile({ root: root, props: props }); return; } text(root.ref.fileSize, root.query('GET_LABEL_FILE_SIZE_NOT_AVAILABLE')); }; var fileInfo = createView({ name: 'file-info', ignoreRect: true, ignoreRectUpdate: true, write: createRoute({ DID_LOAD_ITEM: updateFile, DID_UPDATE_ITEM_META: updateFile, DID_THROW_ITEM_LOAD_ERROR: updateFileSizeOnError, DID_THROW_ITEM_INVALID: updateFileSizeOnError, }), didCreateView: function didCreateView(root) { applyFilters('CREATE_VIEW', Object.assign({}, root, { view: root })); }, create: create$2, mixins: { styles: ['translateX', 'translateY'], animations: { translateX: 'spring', translateY: 'spring', }, }, }); var toPercentage = function toPercentage(value) { return Math.round(value * 100); }; var create$3 = function create(_ref) { var root = _ref.root; // main status var main = createElement$1('span'); main.className = 'filepond--file-status-main'; root.appendChild(main); root.ref.main = main; // sub status var sub = createElement$1('span'); sub.className = 'filepond--file-status-sub'; root.appendChild(sub); root.ref.sub = sub; didSetItemLoadProgress({ root: root, action: { progress: null } }); }; var didSetItemLoadProgress = function didSetItemLoadProgress(_ref2) { var root = _ref2.root, action = _ref2.action; var title = action.progress === null ? root.query('GET_LABEL_FILE_LOADING') : root.query('GET_LABEL_FILE_LOADING') + ' ' + toPercentage(action.progress) + '%'; text(root.ref.main, title); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL')); }; var didSetItemProcessProgress = function didSetItemProcessProgress(_ref3) { var root = _ref3.root, action = _ref3.action; var title = action.progress === null ? root.query('GET_LABEL_FILE_PROCESSING') : root.query('GET_LABEL_FILE_PROCESSING') + ' ' + toPercentage(action.progress) + '%'; text(root.ref.main, title); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL')); }; var didRequestItemProcessing = function didRequestItemProcessing(_ref4) { var root = _ref4.root; text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING')); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL')); }; var didAbortItemProcessing = function didAbortItemProcessing(_ref5) { var root = _ref5.root; text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_ABORTED')); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_RETRY')); }; var didCompleteItemProcessing = function didCompleteItemProcessing(_ref6) { var root = _ref6.root; text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_COMPLETE')); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_UNDO')); }; var clear = function clear(_ref7) { var root = _ref7.root; text(root.ref.main, ''); text(root.ref.sub, ''); }; var error = function error(_ref8) { var root = _ref8.root, action = _ref8.action; text(root.ref.main, action.status.main); text(root.ref.sub, action.status.sub); }; var fileStatus = createView({ name: 'file-status', ignoreRect: true, ignoreRectUpdate: true, write: createRoute({ DID_LOAD_ITEM: clear, DID_REVERT_ITEM_PROCESSING: clear, DID_REQUEST_ITEM_PROCESSING: didRequestItemProcessing, DID_ABORT_ITEM_PROCESSING: didAbortItemProcessing, DID_COMPLETE_ITEM_PROCESSING: didCompleteItemProcessing, DID_UPDATE_ITEM_PROCESS_PROGRESS: didSetItemProcessProgress, DID_UPDATE_ITEM_LOAD_PROGRESS: didSetItemLoadProgress, DID_THROW_ITEM_LOAD_ERROR: error, DID_THROW_ITEM_INVALID: error, DID_THROW_ITEM_PROCESSING_ERROR: error, DID_THROW_ITEM_PROCESSING_REVERT_ERROR: error, DID_THROW_ITEM_REMOVE_ERROR: error, }), didCreateView: function didCreateView(root) { applyFilters('CREATE_VIEW', Object.assign({}, root, { view: root })); }, create: create$3, mixins: { styles: ['translateX', 'translateY', 'opacity'], animations: { opacity: { type: 'tween', duration: 250 }, translateX: 'spring', translateY: 'spring', }, }, }); /** * Button definitions for the file view */ var Buttons = { AbortItemLoad: { label: 'GET_LABEL_BUTTON_ABORT_ITEM_LOAD', action: 'ABORT_ITEM_LOAD', className: 'filepond--action-abort-item-load', align: 'LOAD_INDICATOR_POSITION', // right }, RetryItemLoad: { label: 'GET_LABEL_BUTTON_RETRY_ITEM_LOAD', action: 'RETRY_ITEM_LOAD', icon: 'GET_ICON_RETRY', className: 'filepond--action-retry-item-load', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, RemoveItem: { label: 'GET_LABEL_BUTTON_REMOVE_ITEM', action: 'REQUEST_REMOVE_ITEM', icon: 'GET_ICON_REMOVE', className: 'filepond--action-remove-item', align: 'BUTTON_REMOVE_ITEM_POSITION', // left }, ProcessItem: { label: 'GET_LABEL_BUTTON_PROCESS_ITEM', action: 'REQUEST_ITEM_PROCESSING', icon: 'GET_ICON_PROCESS', className: 'filepond--action-process-item', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, AbortItemProcessing: { label: 'GET_LABEL_BUTTON_ABORT_ITEM_PROCESSING', action: 'ABORT_ITEM_PROCESSING', className: 'filepond--action-abort-item-processing', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, RetryItemProcessing: { label: 'GET_LABEL_BUTTON_RETRY_ITEM_PROCESSING', action: 'RETRY_ITEM_PROCESSING', icon: 'GET_ICON_RETRY', className: 'filepond--action-retry-item-processing', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, RevertItemProcessing: { label: 'GET_LABEL_BUTTON_UNDO_ITEM_PROCESSING', action: 'REQUEST_REVERT_ITEM_PROCESSING', icon: 'GET_ICON_UNDO', className: 'filepond--action-revert-item-processing', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, }; // make a list of buttons, we can then remove buttons from this list if they're disabled var ButtonKeys = []; forin(Buttons, function(key) { ButtonKeys.push(key); }); var calculateFileInfoOffset = function calculateFileInfoOffset(root) { if (getRemoveIndicatorAligment(root) === 'right') return 0; var buttonRect = root.ref.buttonRemoveItem.rect.element; return buttonRect.hidden ? null : buttonRect.width + buttonRect.left; }; var calculateButtonWidth = function calculateButtonWidth(root) { var buttonRect = root.ref.buttonAbortItemLoad.rect.element; return buttonRect.width; }; // Force on full pixels so text stays crips var calculateFileVerticalCenterOffset = function calculateFileVerticalCenterOffset(root) { return Math.floor(root.ref.buttonRemoveItem.rect.element.height / 4); }; var calculateFileHorizontalCenterOffset = function calculateFileHorizontalCenterOffset(root) { return Math.floor(root.ref.buttonRemoveItem.rect.element.left / 2); }; var getLoadIndicatorAlignment = function getLoadIndicatorAlignment(root) { return root.query('GET_STYLE_LOAD_INDICATOR_POSITION'); }; var getProcessIndicatorAlignment = function getProcessIndicatorAlignment(root) { return root.query('GET_STYLE_PROGRESS_INDICATOR_POSITION'); }; var getRemoveIndicatorAligment = function getRemoveIndicatorAligment(root) { return root.query('GET_STYLE_BUTTON_REMOVE_ITEM_POSITION'); }; var DefaultStyle = { buttonAbortItemLoad: { opacity: 0 }, buttonRetryItemLoad: { opacity: 0 }, buttonRemoveItem: { opacity: 0 }, buttonProcessItem: { opacity: 0 }, buttonAbortItemProcessing: { opacity: 0 }, buttonRetryItemProcessing: { opacity: 0 }, buttonRevertItemProcessing: { opacity: 0 }, loadProgressIndicator: { opacity: 0, align: getLoadIndicatorAlignment }, processProgressIndicator: { opacity: 0, align: getProcessIndicatorAlignment }, processingCompleteIndicator: { opacity: 0, scaleX: 0.75, scaleY: 0.75 }, info: { translateX: 0, translateY: 0, opacity: 0 }, status: { translateX: 0, translateY: 0, opacity: 0 }, }; var IdleStyle = { buttonRemoveItem: { opacity: 1 }, buttonProcessItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { translateX: calculateFileInfoOffset }, }; var ProcessingStyle = { buttonAbortItemProcessing: { opacity: 1 }, processProgressIndicator: { opacity: 1 }, status: { opacity: 1 }, }; var StyleMap = { DID_THROW_ITEM_INVALID: { buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { translateX: calculateFileInfoOffset, opacity: 1 }, }, DID_START_ITEM_LOAD: { buttonAbortItemLoad: { opacity: 1 }, loadProgressIndicator: { opacity: 1 }, status: { opacity: 1 }, }, DID_THROW_ITEM_LOAD_ERROR: { buttonRetryItemLoad: { opacity: 1 }, buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 1 }, }, DID_START_ITEM_REMOVE: { processProgressIndicator: { opacity: 1, align: getRemoveIndicatorAligment }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 0 }, }, DID_THROW_ITEM_REMOVE_ERROR: { processProgressIndicator: { opacity: 0, align: getRemoveIndicatorAligment }, buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 1, translateX: calculateFileInfoOffset }, }, DID_LOAD_ITEM: IdleStyle, DID_LOAD_LOCAL_ITEM: { buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { translateX: calculateFileInfoOffset }, }, DID_START_ITEM_PROCESSING: ProcessingStyle, DID_REQUEST_ITEM_PROCESSING: ProcessingStyle, DID_UPDATE_ITEM_PROCESS_PROGRESS: ProcessingStyle, DID_COMPLETE_ITEM_PROCESSING: { buttonRevertItemProcessing: { opacity: 1 }, info: { opacity: 1 }, status: { opacity: 1 }, }, DID_THROW_ITEM_PROCESSING_ERROR: { buttonRemoveItem: { opacity: 1 }, buttonRetryItemProcessing: { opacity: 1 }, status: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, }, DID_THROW_ITEM_PROCESSING_REVERT_ERROR: { buttonRevertItemProcessing: { opacity: 1 }, status: { opacity: 1 }, info: { opacity: 1 }, }, DID_ABORT_ITEM_PROCESSING: { buttonRemoveItem: { opacity: 1 }, buttonProcessItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 1 }, }, DID_REVERT_ITEM_PROCESSING: IdleStyle, }; // complete indicator view var processingCompleteIndicatorView = createView({ create: function create(_ref) { var root = _ref.root; root.element.innerHTML = root.query('GET_ICON_DONE'); }, name: 'processing-complete-indicator', ignoreRect: true, mixins: { styles: ['scaleX', 'scaleY', 'opacity'], animations: { scaleX: 'spring', scaleY: 'spring', opacity: { type: 'tween', duration: 250 }, }, }, }); /** * Creates the file view */ var create$4 = function create(_ref2) { var root = _ref2.root, props = _ref2.props; // copy Buttons object var LocalButtons = Object.keys(Buttons).reduce(function(prev, curr) { prev[curr] = Object.assign({}, Buttons[curr]); return prev; }, {}); var id = props.id; // allow reverting upload var allowRevert = root.query('GET_ALLOW_REVERT'); // allow remove file var allowRemove = root.query('GET_ALLOW_REMOVE'); // allow processing upload var allowProcess = root.query('GET_ALLOW_PROCESS'); // is instant uploading, need this to determine the icon of the undo button var instantUpload = root.query('GET_INSTANT_UPLOAD'); // is async set up var isAsync = root.query('IS_ASYNC'); // should align remove item buttons var alignRemoveItemButton = root.query('GET_STYLE_BUTTON_REMOVE_ITEM_ALIGN'); // enabled buttons array var buttonFilter; if (isAsync) { if (allowProcess && !allowRevert) { // only remove revert button buttonFilter = function buttonFilter(key) { return !/RevertItemProcessing/.test(key); }; } else if (!allowProcess && allowRevert) { // only remove process button buttonFilter = function buttonFilter(key) { return !/ProcessItem|RetryItemProcessing|AbortItemProcessing/.test(key); }; } else if (!allowProcess && !allowRevert) { // remove all process buttons buttonFilter = function buttonFilter(key) { return !/Process/.test(key); }; } } else { // no process controls available buttonFilter = function buttonFilter(key) { return !/Process/.test(key); }; } var enabledButtons = buttonFilter ? ButtonKeys.filter(buttonFilter) : ButtonKeys.concat(); // update icon and label for revert button when instant uploading if (instantUpload && allowRevert) { LocalButtons['RevertItemProcessing'].label = 'GET_LABEL_BUTTON_REMOVE_ITEM'; LocalButtons['RevertItemProcessing'].icon = 'GET_ICON_REMOVE'; } // remove last button (revert) if not allowed if (isAsync && !allowRevert) { var map = StyleMap['DID_COMPLETE_ITEM_PROCESSING']; map.info.translateX = calculateFileHorizontalCenterOffset; map.info.translateY = calculateFileVerticalCenterOffset; map.status.translateY = calculateFileVerticalCenterOffset; map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 }; } // should align center if (isAsync && !allowProcess) { [ 'DID_START_ITEM_PROCESSING', 'DID_REQUEST_ITEM_PROCESSING', 'DID_UPDATE_ITEM_PROCESS_PROGRESS', 'DID_THROW_ITEM_PROCESSING_ERROR', ].forEach(function(key) { StyleMap[key].status.translateY = calculateFileVerticalCenterOffset; }); StyleMap['DID_THROW_ITEM_PROCESSING_ERROR'].status.translateX = calculateButtonWidth; } // move remove button to right if (alignRemoveItemButton && allowRevert) { LocalButtons['RevertItemProcessing'].align = 'BUTTON_REMOVE_ITEM_POSITION'; var _map = StyleMap['DID_COMPLETE_ITEM_PROCESSING']; _map.info.translateX = calculateFileInfoOffset; _map.status.translateY = calculateFileVerticalCenterOffset; _map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 }; } // show/hide RemoveItem button if (!allowRemove) { LocalButtons['RemoveItem'].disabled = true; } // create the button views forin(LocalButtons, function(key, definition) { // create button var buttonView = root.createChildView(fileActionButton, { label: root.query(definition.label), icon: root.query(definition.icon), opacity: 0, }); // should be appended? if (enabledButtons.includes(key)) { root.appendChildView(buttonView); } // toggle if (definition.disabled) { buttonView.element.setAttribute('disabled', 'disabled'); buttonView.element.setAttribute('hidden', 'hidden'); } // add position attribute buttonView.element.dataset.align = root.query('GET_STYLE_' + definition.align); // add class buttonView.element.classList.add(definition.className); // handle interactions buttonView.on('click', function(e) { e.stopPropagation(); if (definition.disabled) return; root.dispatch(definition.action, { query: id }); }); // set reference root.ref['button' + key] = buttonView; }); // checkmark root.ref.processingCompleteIndicator = root.appendChildView( root.createChildView(processingCompleteIndicatorView) ); root.ref.processingCompleteIndicator.element.dataset.align = root.query( 'GET_STYLE_BUTTON_PROCESS_ITEM_POSITION' ); // create file info view root.ref.info = root.appendChildView(root.createChildView(fileInfo, { id: id })); // create file status view root.ref.status = root.appendChildView(root.createChildView(fileStatus, { id: id })); // add progress indicators var loadIndicatorView = root.appendChildView( root.createChildView(progressIndicator, { opacity: 0, align: root.query('GET_STYLE_LOAD_INDICATOR_POSITION'), }) ); loadIndicatorView.element.classList.add('filepond--load-indicator'); root.ref.loadProgressIndicator = loadIndicatorView; var progressIndicatorView = root.appendChildView( root.createChildView(progressIndicator, { opacity: 0, align: root.query('GET_STYLE_PROGRESS_INDICATOR_POSITION'), }) ); progressIndicatorView.element.classList.add('filepond--process-indicator'); root.ref.processProgressIndicator = progressIndicatorView; // current active styles root.ref.activeStyles = []; }; var write$2 = function write(_ref3) { var root = _ref3.root, actions = _ref3.actions, props = _ref3.props; // route actions route({ root: root, actions: actions, props: props }); // select last state change action var action = actions .concat() .filter(function(action) { return /^DID_/.test(action.type); }) .reverse() .find(function(action) { return StyleMap[action.type]; }); // a new action happened, let's get the matching styles if (action) { // define new active styles root.ref.activeStyles = []; var stylesToApply = StyleMap[action.type]; forin(DefaultStyle, function(name, defaultStyles) { // get reference to control var control = root.ref[name]; // loop over all styles for this control forin(defaultStyles, function(key, defaultValue) { var value = stylesToApply[name] && typeof stylesToApply[name][key] !== 'undefined' ? stylesToApply[name][key] : defaultValue; root.ref.activeStyles.push({ control: control, key: key, value: value }); }); }); } // apply active styles to element root.ref.activeStyles.forEach(function(_ref4) { var control = _ref4.control, key = _ref4.key, value = _ref4.value; control[key] = typeof value === 'function' ? value(root) : value; }); }; var route = createRoute({ DID_SET_LABEL_BUTTON_ABORT_ITEM_PROCESSING: function DID_SET_LABEL_BUTTON_ABORT_ITEM_PROCESSING( _ref5 ) { var root = _ref5.root, action = _ref5.action; root.ref.buttonAbortItemProcessing.label = action.value; }, DID_SET_LABEL_BUTTON_ABORT_ITEM_LOAD: function DID_SET_LABEL_BUTTON_ABORT_ITEM_LOAD(_ref6) { var root = _ref6.root, action = _ref6.action; root.ref.buttonAbortItemLoad.label = action.value; }, DID_SET_LABEL_BUTTON_ABORT_ITEM_REMOVAL: function DID_SET_LABEL_BUTTON_ABORT_ITEM_REMOVAL( _ref7 ) { var root = _ref7.root, action = _ref7.action; root.ref.buttonAbortItemRemoval.label = action.value; }, DID_REQUEST_ITEM_PROCESSING: function DID_REQUEST_ITEM_PROCESSING(_ref8) { var root = _ref8.root; root.ref.processProgressIndicator.spin = true; root.ref.processProgressIndicator.progress = 0; }, DID_START_ITEM_LOAD: function DID_START_ITEM_LOAD(_ref9) { var root = _ref9.root; root.ref.loadProgressIndicator.spin = true; root.ref.loadProgressIndicator.progress = 0; }, DID_START_ITEM_REMOVE: function DID_START_ITEM_REMOVE(_ref10) { var root = _ref10.root; root.ref.processProgressIndicator.spin = true; root.ref.processProgressIndicator.progress = 0; }, DID_UPDATE_ITEM_LOAD_PROGRESS: function DID_UPDATE_ITEM_LOAD_PROGRESS(_ref11) { var root = _ref11.root, action = _ref11.action; root.ref.loadProgressIndicator.spin = false; root.ref.loadProgressIndicator.progress = action.progress; }, DID_UPDATE_ITEM_PROCESS_PROGRESS: function DID_UPDATE_ITEM_PROCESS_PROGRESS(_ref12) { var root = _ref12.root, action = _ref12.action; root.ref.processProgressIndicator.spin = false; root.ref.processProgressIndicator.progress = action.progress; }, }); var file = createView({ create: create$4, write: write$2, didCreateView: function didCreateView(root) { applyFilters('CREATE_VIEW', Object.assign({}, root, { view: root })); }, name: 'file', }); /** * Creates the file view */ var create$5 = function create(_ref) { var root = _ref.root, props = _ref.props; // filename root.ref.fileName = createElement$1('legend'); root.appendChild(root.ref.fileName); // file appended root.ref.file = root.appendChildView(root.createChildView(file, { id: props.id })); // data has moved to data.js root.ref.data = false; }; /** * Data storage */ var didLoadItem = function didLoadItem(_ref2) { var root = _ref2.root, props = _ref2.props; // updates the legend of the fieldset so screenreaders can better group buttons text(root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id))); }; var fileWrapper = createView({ create: create$5, ignoreRect: true, write: createRoute({ DID_LOAD_ITEM: didLoadItem, }), didCreateView: function didCreateView(root) { applyFilters('CREATE_VIEW', Object.assign({}, root, { view: root })); }, tag: 'fieldset', name: 'file-wrapper', }); var PANEL_SPRING_PROPS = { type: 'spring', damping: 0.6, mass: 7 }; var create$6 = function create(_ref) { var root = _ref.root, props = _ref.props; [ { name: 'top', }, { name: 'center', props: { translateY: null, scaleY: null, }, mixins: { animations: { scaleY: PANEL_SPRING_PROPS, }, styles: ['translateY', 'scaleY'], }, }, { name: 'bottom', props: { translateY: null, }, mixins: { animations: { translateY: PANEL_SPRING_PROPS, }, styles: ['translateY'], }, }, ].forEach(function(section) { createSection(root, section, props.name); }); root.element.classList.add('filepond--' + props.name); root.ref.scalable = null; }; var createSection = function createSection(root, section, className) { var viewConstructor = createView({ name: 'panel-' + section.name + ' filepond--' + className, mixins: section.mixins, ignoreRectUpdate: true, }); var view = root.createChildView(viewConstructor, section.props); root.ref[section.name] = root.appendChildView(view); }; var write$3 = function write(_ref2) { var root = _ref2.root, props = _ref2.props; // update scalable state if (root.ref.scalable === null || props.scalable !== root.ref.scalable) { root.ref.scalable = isBoolean(props.scalable) ? props.scalable : true; root.element.dataset.scalable = root.ref.scalable; } // no height, can't set if (!props.height) return; // get child rects var topRect = root.ref.top.rect.element; var bottomRect = root.ref.bottom.rect.element; // make sure height never is smaller than bottom and top seciton heights combined (will probably never happen, but who knows) var height = Math.max(topRect.height + bottomRect.height, props.height); // offset center part root.ref.center.translateY = topRect.height; // scale center part // use math ceil to prevent transparent lines because of rounding errors root.ref.center.scaleY = (height - topRect.height - bottomRect.height) / 100; // offset bottom part root.ref.bottom.translateY = height - bottomRect.height; }; var panel = createView({ name: 'panel', read: function read(_ref3) { var root = _ref3.root, props = _ref3.props; return (props.heightCurrent = root.ref.bottom.translateY); }, write: write$3, create: create$6, ignoreRect: true, mixins: { apis: ['height', 'heightCurrent', 'scalable'], }, }); var createDragHelper = function createDragHelper(items) { var itemIds = items.map(function(item) { return item.id; }); var prevIndex = undefined; return { setIndex: function setIndex(index) { prevIndex = index; }, getIndex: function getIndex() { return prevIndex; }, getItemIndex: function getItemIndex(item) { return itemIds.indexOf(item.id); }, }; }; var ITEM_TRANSLATE_SPRING = { type: 'spring', stiffness: 0.75, damping: 0.45, mass: 10, }; var ITEM_SCALE_SPRING = 'spring'; var StateMap = { DID_START_ITEM_LOAD: 'busy', DID_UPDATE_ITEM_LOAD_PROGRESS: 'loading', DID_THROW_ITEM_INVALID: 'load-invalid', DID_THROW_ITEM_LOAD_ERROR: 'load-error', DID_LOAD_ITEM: 'idle', DID_THROW_ITEM_REMOVE_ERROR: 'remove-error', DID_START_ITEM_REMOVE: 'busy', DID_START_ITEM_PROCESSING: 'busy processing', DID_REQUEST_ITEM_PROCESSING: 'busy processing', DID_UPDATE_ITEM_PROCESS_PROGRESS: 'processing', DID_COMPLETE_ITEM_PROCESSING: 'processing-complete', DID_THROW_ITEM_PROCESSING_ERROR: 'processing-error', DID_THROW_ITEM_PROCESSING_REVERT_ERROR: 'processing-revert-error', DID_ABORT_ITEM_PROCESSING: 'cancelled', DID_REVERT_ITEM_PROCESSING: 'idle', }; /** * Creates the file view */ var create$7 = function create(_ref) { var root = _ref.root, props = _ref.props; // select root.ref.handleClick = function(e) { return root.dispatch('DID_ACTIVATE_ITEM', { id: props.id }); }; // set id root.element.id = 'filepond--item-' + props.id; root.element.addEventListener('click', root.ref.handleClick); // file view root.ref.container = root.appendChildView( root.createChildView(fileWrapper, { id: props.id }) ); // file panel root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'item-panel' })); // default start height root.ref.panel.height = null; // by default not marked for removal props.markedForRemoval = false; // if not allowed to reorder file items, exit here if (!root.query('GET_ALLOW_REORDER')) return; // set to idle so shows grab cursor root.element.dataset.dragState = 'idle'; var grab = function grab(e) { if (!e.isPrimary) return; var removedActivateListener = false; var origin = { x: e.pageX, y: e.pageY, }; props.dragOrigin = { x: root.translateX, y: root.translateY, }; props.dragCenter = { x: e.offsetX, y: e.offsetY, }; var dragState = createDragHelper(root.query('GET_ACTIVE_ITEMS')); root.dispatch('DID_GRAB_ITEM', { id: props.id, dragState: dragState }); var drag = function drag(e) { if (!e.isPrimary) return; e.stopPropagation(); e.preventDefault(); props.dragOffset = { x: e.pageX - origin.x, y: e.pageY - origin.y, }; // if dragged stop listening to clicks, will re-add when done dragging var dist = props.dragOffset.x * props.dragOffset.x + props.dragOffset.y * props.dragOffset.y; if (dist > 16 && !removedActivateListener) { removedActivateListener = true; root.element.removeEventListener('click', root.ref.handleClick); } root.dispatch('DID_DRAG_ITEM', { id: props.id, dragState: dragState }); }; var drop = function drop(e) { if (!e.isPrimary) return; props.dragOffset = { x: e.pageX - origin.x, y: e.pageY - origin.y, }; reset(); }; var cancel = function cancel() { reset(); }; var reset = function reset() { document.removeEventListener('pointercancel', cancel); document.removeEventListener('pointermove', drag); document.removeEventListener('pointerup', drop); root.dispatch('DID_DROP_ITEM', { id: props.id, dragState: dragState }); // start listening to clicks again if (removedActivateListener) { setTimeout(function() { return root.element.addEventListener('click', root.ref.handleClick); }, 0); } }; document.addEventListener('pointercancel', cancel); document.addEventListener('pointermove', drag); document.addEventListener('pointerup', drop); }; root.element.addEventListener('pointerdown', grab); }; var route$1 = createRoute({ DID_UPDATE_PANEL_HEIGHT: function DID_UPDATE_PANEL_HEIGHT(_ref2) { var root = _ref2.root, action = _ref2.action; root.height = action.height; }, }); var write$4 = createRoute( { DID_GRAB_ITEM: function DID_GRAB_ITEM(_ref3) { var root = _ref3.root, props = _ref3.props; props.dragOrigin = { x: root.translateX, y: root.translateY, }; }, DID_DRAG_ITEM: function DID_DRAG_ITEM(_ref4) { var root = _ref4.root; root.element.dataset.dragState = 'drag'; }, DID_DROP_ITEM: function DID_DROP_ITEM(_ref5) { var root = _ref5.root, props = _ref5.props; props.dragOffset = null; props.dragOrigin = null; root.element.dataset.dragState = 'drop'; }, }, function(_ref6) { var root = _ref6.root, actions = _ref6.actions, props = _ref6.props, shouldOptimize = _ref6.shouldOptimize; if (root.element.dataset.dragState === 'drop') { if (root.scaleX <= 1) { root.element.dataset.dragState = 'idle'; } } // select last state change action var action = actions .concat() .filter(function(action) { return /^DID_/.test(action.type); }) .reverse() .find(function(action) { return StateMap[action.type]; }); // no need to set same state twice if (action && action.type !== props.currentState) { // set current state props.currentState = action.type; // set state root.element.dataset.filepondItemState = StateMap[props.currentState] || ''; } // route actions var aspectRatio = root.query('GET_ITEM_PANEL_ASPECT_RATIO') || root.query('GET_PANEL_ASPECT_RATIO'); if (!aspectRatio) { route$1({ root: root, actions: actions, props: props }); if (!root.height && root.ref.container.rect.element.height > 0) { root.height = root.ref.container.rect.element.height; } } else if (!shouldOptimize) { root.height = root.rect.element.width * aspectRatio; } // sync panel height with item height if (shouldOptimize) { root.ref.panel.height = null; } root.ref.panel.height = root.height; } ); var item = createView({ create: create$7, write: write$4, destroy: function destroy(_ref7) { var root = _ref7.root, props = _ref7.props; root.element.removeEventListener('click', root.ref.handleClick); root.dispatch('RELEASE_ITEM', { query: props.id }); }, tag: 'li', name: 'item', mixins: { apis: [ 'id', 'interactionMethod', 'markedForRemoval', 'spawnDate', 'dragCenter', 'dragOrigin', 'dragOffset', ], styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity', 'height'], animations: { scaleX: ITEM_SCALE_SPRING, scaleY: ITEM_SCALE_SPRING, translateX: ITEM_TRANSLATE_SPRING, translateY: ITEM_TRANSLATE_SPRING, opacity: { type: 'tween', duration: 150 }, }, }, }); var getItemsPerRow = function(horizontalSpace, itemWidth) { // add one pixel leeway, when using percentages for item width total items can be 1.99 per row return Math.max(1, Math.floor((horizontalSpace + 1) / itemWidth)); }; var getItemIndexByPosition = function getItemIndexByPosition(view, children, positionInView) { if (!positionInView) return; var horizontalSpace = view.rect.element.width; // const children = view.childViews; var l = children.length; var last = null; // -1, don't move items to accomodate (either add to top or bottom) if (l === 0 || positionInView.top < children[0].rect.element.top) return -1; // let's get the item width var item = children[0]; var itemRect = item.rect.element; var itemHorizontalMargin = itemRect.marginLeft + itemRect.marginRight; var itemWidth = itemRect.width + itemHorizontalMargin; var itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth); // stack if (itemsPerRow === 1) { for (var index = 0; index < l; index++) { var child = children[index]; var childMid = child.rect.outer.top + child.rect.element.height * 0.5; if (positionInView.top < childMid) { return index; } } return l; } // grid var itemVerticalMargin = itemRect.marginTop + itemRect.marginBottom; var itemHeight = itemRect.height + itemVerticalMargin; for (var _index = 0; _index < l; _index++) { var indexX = _index % itemsPerRow; var indexY = Math.floor(_index / itemsPerRow); var offsetX = indexX * itemWidth; var offsetY = indexY * itemHeight; var itemTop = offsetY - itemRect.marginTop; var itemRight = offsetX + itemWidth; var itemBottom = offsetY + itemHeight + itemRect.marginBottom; if (positionInView.top < itemBottom && positionInView.top > itemTop) { if (positionInView.left < itemRight) { return _index; } else if (_index !== l - 1) { last = _index; } else { last = null; } } } if (last !== null) { return last; } return l; }; var dropAreaDimensions = { height: 0, width: 0, get getHeight() { return this.height; }, set setHeight(val) { if (this.height === 0 || val === 0) this.height = val; }, get getWidth() { return this.width; }, set setWidth(val) { if (this.width === 0 || val === 0) this.width = val; }, setDimensions: function setDimensions(height, width) { if (this.height === 0 || height === 0) this.height = height; if (this.width === 0 || width === 0) this.width = width; }, }; var create$8 = function create(_ref) { var root = _ref.root; // need to set role to list as otherwise it won't be read as a list by VoiceOver attr(root.element, 'role', 'list'); root.ref.lastItemSpanwDate = Date.now(); }; /** * Inserts a new item * @param root * @param action */ var addItemView = function addItemView(_ref2) { var root = _ref2.root, action = _ref2.action; var id = action.id, index = action.index, interactionMethod = action.interactionMethod; root.ref.addIndex = index; var now = Date.now(); var spawnDate = now; var opacity = 1; if (interactionMethod !== InteractionMethod.NONE) { opacity = 0; var cooldown = root.query('GET_ITEM_INSERT_INTERVAL'); var dist = now - root.ref.lastItemSpanwDate; spawnDate = dist < cooldown ? now + (cooldown - dist) : now; } root.ref.lastItemSpanwDate = spawnDate; root.appendChildView( root.createChildView( // view type item, // props { spawnDate: spawnDate, id: id, opacity: opacity, interactionMethod: interactionMethod, } ), index ); }; var moveItem = function moveItem(item, x, y) { var vx = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; var vy = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; // set to null to remove animation while dragging if (item.dragOffset) { item.translateX = null; item.translateY = null; item.translateX = item.dragOrigin.x + item.dragOffset.x; item.translateY = item.dragOrigin.y + item.dragOffset.y; item.scaleX = 1.025; item.scaleY = 1.025; } else { item.translateX = x; item.translateY = y; if (Date.now() > item.spawnDate) { // reveal element if (item.opacity === 0) { introItemView(item, x, y, vx, vy); } // make sure is default scale every frame item.scaleX = 1; item.scaleY = 1; item.opacity = 1; } } }; var introItemView = function introItemView(item, x, y, vx, vy) { if (item.interactionMethod === InteractionMethod.NONE) { item.translateX = null; item.translateX = x; item.translateY = null; item.translateY = y; } else if (item.interactionMethod === InteractionMethod.DROP) { item.translateX = null; item.translateX = x - vx * 20; item.translateY = null; item.translateY = y - vy * 10; item.scaleX = 0.8; item.scaleY = 0.8; } else if (item.interactionMethod === InteractionMethod.BROWSE) { item.translateY = null; item.translateY = y - 30; } else if (item.interactionMethod === InteractionMethod.API) { item.translateX = null; item.translateX = x - 30; item.translateY = null; } }; /** * Removes an existing item * @param root * @param action */ var removeItemView = function removeItemView(_ref3) { var root = _ref3.root, action = _ref3.action; var id = action.id; // get the view matching the given id var view = root.childViews.find(function(child) { return child.id === id; }); // if no view found, exit if (!view) { return; } // animate view out of view view.scaleX = 0.9; view.scaleY = 0.9; view.opacity = 0; // mark for removal view.markedForRemoval = true; }; var getItemHeight = function getItemHeight(child) { return ( child.rect.element.height + child.rect.element.marginBottom + child.rect.element.marginTop ); }; var getItemWidth = function getItemWidth(child) { return ( child.rect.element.width + child.rect.element.marginLeft * 0.5 + child.rect.element.marginRight * 0.5 ); }; var dragItem = function dragItem(_ref4) { var root = _ref4.root, action = _ref4.action; var id = action.id, dragState = action.dragState; // reference to item var item = root.query('GET_ITEM', { id: id }); // get the view matching the given id var view = root.childViews.find(function(child) { return child.id === id; }); var numItems = root.childViews.length; var oldIndex = dragState.getItemIndex(item); // if no view found, exit if (!view) return; var dragPosition = { x: view.dragOrigin.x + view.dragOffset.x + view.dragCenter.x, y: view.dragOrigin.y + view.dragOffset.y + view.dragCenter.y, }; // get drag area dimensions var dragHeight = getItemHeight(view); var dragWidth = getItemWidth(view); // get rows and columns (There will always be at least one row and one column if a file is present) var cols = Math.floor(root.rect.outer.width / dragWidth); if (cols > numItems) cols = numItems; // rows are used to find when we have left the preview area bounding box var rows = Math.floor(numItems / cols + 1); dropAreaDimensions.setHeight = dragHeight * rows; dropAreaDimensions.setWidth = dragWidth * cols; // get new index of dragged item var location = { y: Math.floor(dragPosition.y / dragHeight), x: Math.floor(dragPosition.x / dragWidth), getGridIndex: function getGridIndex() { if ( dragPosition.y > dropAreaDimensions.getHeight || dragPosition.y < 0 || dragPosition.x > dropAreaDimensions.getWidth || dragPosition.x < 0 ) return oldIndex; return this.y * cols + this.x; }, getColIndex: function getColIndex() { var items = root.query('GET_ACTIVE_ITEMS'); var visibleChildren = root.childViews.filter(function(child) { return child.rect.element.height; }); var children = items.map(function(item) { return visibleChildren.find(function(childView) { return childView.id === item.id; }); }); var currentIndex = children.findIndex(function(child) { return child === view; }); var dragHeight = getItemHeight(view); var l = children.length; var idx = l; var childHeight = 0; var childBottom = 0; var childTop = 0; for (var i = 0; i < l; i++) { childHeight = getItemHeight(children[i]); childTop = childBottom; childBottom = childTop + childHeight; if (dragPosition.y < childBottom) { if (currentIndex > i) { if (dragPosition.y < childTop + dragHeight) { idx = i; break; } continue; } idx = i; break; } } return idx; }, }; // get new index var index = cols > 1 ? location.getGridIndex() : location.getColIndex(); root.dispatch('MOVE_ITEM', { query: view, index: index }); // if the index of the item changed, dispatch reorder action var currentIndex = dragState.getIndex(); if (currentIndex === undefined || currentIndex !== index) { dragState.setIndex(index); if (currentIndex === undefined) return; root.dispatch('DID_REORDER_ITEMS', { items: root.query('GET_ACTIVE_ITEMS'), origin: oldIndex, target: index, }); } }; /** * Setup action routes */ var route$2 = createRoute({ DID_ADD_ITEM: addItemView, DID_REMOVE_ITEM: removeItemView, DID_DRAG_ITEM: dragItem, }); /** * Write to view * @param root * @param actions * @param props */ var write$5 = function write(_ref5) { var root = _ref5.root, props = _ref5.props, actions = _ref5.actions, shouldOptimize = _ref5.shouldOptimize; // route actions route$2({ root: root, props: props, actions: actions }); var dragCoordinates = props.dragCoordinates; // available space on horizontal axis var horizontalSpace = root.rect.element.width; // only draw children that have dimensions var visibleChildren = root.childViews.filter(function(child) { return child.rect.element.height; }); // sort based on current active items var children = root .query('GET_ACTIVE_ITEMS') .map(function(item) { return visibleChildren.find(function(child) { return child.id === item.id; }); }) .filter(function(item) { return item; }); // get index var dragIndex = dragCoordinates ? getItemIndexByPosition(root, children, dragCoordinates) : null; // add index is used to reserve the dropped/added item index till the actual item is rendered var addIndex = root.ref.addIndex || null; // add index no longer needed till possibly next draw root.ref.addIndex = null; var dragIndexOffset = 0; var removeIndexOffset = 0; var addIndexOffset = 0; if (children.length === 0) return; var childRect = children[0].rect.element; var itemVerticalMargin = childRect.marginTop + childRect.marginBottom; var itemHorizontalMargin = childRect.marginLeft + childRect.marginRight; var itemWidth = childRect.width + itemHorizontalMargin; var itemHeight = childRect.height + itemVerticalMargin; var itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth); // stack if (itemsPerRow === 1) { var offsetY = 0; var dragOffset = 0; children.forEach(function(child, index) { if (dragIndex) { var dist = index - dragIndex; if (dist === -2) { dragOffset = -itemVerticalMargin * 0.25; } else if (dist === -1) { dragOffset = -itemVerticalMargin * 0.75; } else if (dist === 0) { dragOffset = itemVerticalMargin * 0.75; } else if (dist === 1) { dragOffset = itemVerticalMargin * 0.25; } else { dragOffset = 0; } } if (shouldOptimize) { child.translateX = null; child.translateY = null; } if (!child.markedForRemoval) { moveItem(child, 0, offsetY + dragOffset); } var itemHeight = child.rect.element.height + itemVerticalMargin; var visualHeight = itemHeight * (child.markedForRemoval ? child.opacity : 1); offsetY += visualHeight; }); } // grid else { var prevX = 0; var prevY = 0; children.forEach(function(child, index) { if (index === dragIndex) { dragIndexOffset = 1; } if (index === addIndex) { addIndexOffset += 1; } if (child.markedForRemoval && child.opacity < 0.5) { removeIndexOffset -= 1; } var visualIndex = index + addIndexOffset + dragIndexOffset + removeIndexOffset; var indexX = visualIndex % itemsPerRow; var indexY = Math.floor(visualIndex / itemsPerRow); var offsetX = indexX * itemWidth; var offsetY = indexY * itemHeight; var vectorX = Math.sign(offsetX - prevX); var vectorY = Math.sign(offsetY - prevY); prevX = offsetX; prevY = offsetY; if (child.markedForRemoval) return; if (shouldOptimize) { child.translateX = null; child.translateY = null; } moveItem(child, offsetX, offsetY, vectorX, vectorY); }); } }; /** * Filters actions that are meant specifically for a certain child of the list * @param child * @param actions */ var filterSetItemActions = function filterSetItemActions(child, actions) { return actions.filter(function(action) { // if action has an id, filter out actions that don't have this child id if (action.data && action.data.id) { return child.id === action.data.id; } // allow all other actions return true; }); }; var list = createView({ create: create$8, write: write$5, tag: 'ul', name: 'list', didWriteView: function didWriteView(_ref6) { var root = _ref6.root; root.childViews .filter(function(view) { return view.markedForRemoval && view.opacity === 0 && view.resting; }) .forEach(function(view) { view._destroy(); root.removeChildView(view); }); }, filterFrameActionsForChild: filterSetItemActions, mixins: { apis: ['dragCoordinates'], }, }); var create$9 = function create(_ref) { var root = _ref.root, props = _ref.props; root.ref.list = root.appendChildView(root.createChildView(list)); props.dragCoordinates = null; props.overflowing = false; }; var storeDragCoordinates = function storeDragCoordinates(_ref2) { var root = _ref2.root, props = _ref2.props, action = _ref2.action; if (!root.query('GET_ITEM_INSERT_LOCATION_FREEDOM')) return; props.dragCoordinates = { left: action.position.scopeLeft - root.ref.list.rect.element.left, top: action.position.scopeTop - (root.rect.outer.top + root.rect.element.marginTop + root.rect.element.scrollTop), }; }; var clearDragCoordinates = function clearDragCoordinates(_ref3) { var props = _ref3.props; props.dragCoordinates = null; }; var route$3 = createRoute({ DID_DRAG: storeDragCoordinates, DID_END_DRAG: clearDragCoordinates, }); var write$6 = function write(_ref4) { var root = _ref4.root, props = _ref4.props, actions = _ref4.actions; // route actions route$3({ root: root, props: props, actions: actions }); // current drag position root.ref.list.dragCoordinates = props.dragCoordinates; // if currently overflowing but no longer received overflow if (props.overflowing && !props.overflow) { props.overflowing = false; // reset overflow state root.element.dataset.state = ''; root.height = null; } // if is not overflowing currently but does receive overflow value if (props.overflow) { var newHeight = Math.round(props.overflow); if (newHeight !== root.height) { props.overflowing = true; root.element.dataset.state = 'overflow'; root.height = newHeight; } } }; var listScroller = createView({ create: create$9, write: write$6, name: 'list-scroller', mixins: { apis: ['overflow', 'dragCoordinates'], styles: ['height', 'translateY'], animations: { translateY: 'spring', }, }, }); var attrToggle = function attrToggle(element, name, state) { var enabledValue = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : ''; if (state) { attr(element, name, enabledValue); } else { element.removeAttribute(name); } }; var resetFileInput = function resetFileInput(input) { // no value, no need to reset if (!input || input.value === '') { return; } try { // for modern browsers input.value = ''; } catch (err) {} // for IE10 if (input.value) { // quickly append input to temp form and reset form var form = createElement$1('form'); var parentNode = input.parentNode; var ref = input.nextSibling; form.appendChild(input); form.reset(); // re-inject input where it originally was if (ref) { parentNode.insertBefore(input, ref); } else { parentNode.appendChild(input); } } }; var create$a = function create(_ref) { var root = _ref.root, props = _ref.props; // set id so can be referenced from outside labels root.element.id = 'filepond--browser-' + props.id; // set name of element (is removed when a value is set) attr(root.element, 'name', root.query('GET_NAME')); // we have to link this element to the status element attr(root.element, 'aria-controls', 'filepond--assistant-' + props.id); // set label, we use labelled by as otherwise the screenreader does not read the "browse" text in the label (as it has tabindex: 0) attr(root.element, 'aria-labelledby', 'filepond--drop-label-' + props.id); // set configurable props setAcceptedFileTypes({ root: root, action: { value: root.query('GET_ACCEPTED_FILE_TYPES') }, }); toggleAllowMultiple({ root: root, action: { value: root.query('GET_ALLOW_MULTIPLE') } }); toggleDirectoryFilter({ root: root, action: { value: root.query('GET_ALLOW_DIRECTORIES_ONLY') }, }); toggleDisabled({ root: root }); toggleRequired({ root: root, action: { value: root.query('GET_REQUIRED') } }); setCaptureMethod({ root: root, action: { value: root.query('GET_CAPTURE_METHOD') } }); // handle changes to the input field root.ref.handleChange = function(e) { if (!root.element.value) { return; } // extract files and move value of webkitRelativePath path to _relativePath var files = Array.from(root.element.files).map(function(file) { file._relativePath = file.webkitRelativePath; return file; }); // we add a little delay so the OS file select window can move out of the way before we add our file setTimeout(function() { // load files props.onload(files); // reset input, it's just for exposing a method to drop files, should not retain any state resetFileInput(root.element); }, 250); }; root.element.addEventListener('change', root.ref.handleChange); }; var setAcceptedFileTypes = function setAcceptedFileTypes(_ref2) { var root = _ref2.root, action = _ref2.action; if (!root.query('GET_ALLOW_SYNC_ACCEPT_ATTRIBUTE')) return; attrToggle( root.element, 'accept', !!action.value, action.value ? action.value.join(',') : '' ); }; var toggleAllowMultiple = function toggleAllowMultiple(_ref3) { var root = _ref3.root, action = _ref3.action; attrToggle(root.element, 'multiple', action.value); }; var toggleDirectoryFilter = function toggleDirectoryFilter(_ref4) { var root = _ref4.root, action = _ref4.action; attrToggle(root.element, 'webkitdirectory', action.value); }; var toggleDisabled = function toggleDisabled(_ref5) { var root = _ref5.root; var isDisabled = root.query('GET_DISABLED'); var doesAllowBrowse = root.query('GET_ALLOW_BROWSE'); var disableField = isDisabled || !doesAllowBrowse; attrToggle(root.element, 'disabled', disableField); }; var toggleRequired = function toggleRequired(_ref6) { var root = _ref6.root, action = _ref6.action; // want to remove required, always possible if (!action.value) { attrToggle(root.element, 'required', false); } // if want to make required, only possible when zero items else if (root.query('GET_TOTAL_ITEMS') === 0) { attrToggle(root.element, 'required', true); } }; var setCaptureMethod = function setCaptureMethod(_ref7) { var root = _ref7.root, action = _ref7.action; attrToggle( root.element, 'capture', !!action.value, action.value === true ? '' : action.value ); }; var updateRequiredStatus = function updateRequiredStatus(_ref8) { var root = _ref8.root; var element = root.element; // always remove the required attribute when more than zero items if (root.query('GET_TOTAL_ITEMS') > 0) { attrToggle(element, 'required', false); attrToggle(element, 'name', false); // still has items var activeItems = root.query('GET_ACTIVE_ITEMS'); var hasInvalidField = false; for (var i = 0; i < activeItems.length; i++) { if (activeItems[i].status === ItemStatus.LOAD_ERROR) { hasInvalidField = true; } } // set validity status root.element.setCustomValidity( hasInvalidField ? root.query('GET_LABEL_INVALID_FIELD') : '' ); } else { // add name attribute attrToggle(element, 'name', true, root.query('GET_NAME')); // remove any validation messages var shouldCheckValidity = root.query('GET_CHECK_VALIDITY'); if (shouldCheckValidity) { element.setCustomValidity(''); } // we only add required if the field has been deemed required if (root.query('GET_REQUIRED')) { attrToggle(element, 'required', true); } } }; var updateFieldValidityStatus = function updateFieldValidityStatus(_ref9) { var root = _ref9.root; var shouldCheckValidity = root.query('GET_CHECK_VALIDITY'); if (!shouldCheckValidity) return; root.element.setCustomValidity(root.query('GET_LABEL_INVALID_FIELD')); }; var browser = createView({ tag: 'input', name: 'browser', ignoreRect: true, ignoreRectUpdate: true, attributes: { type: 'file', }, create: create$a, destroy: function destroy(_ref10) { var root = _ref10.root; root.element.removeEventListener('change', root.ref.handleChange); }, write: createRoute({ DID_LOAD_ITEM: updateRequiredStatus, DID_REMOVE_ITEM: updateRequiredStatus, DID_THROW_ITEM_INVALID: updateFieldValidityStatus, DID_SET_DISABLED: toggleDisabled, DID_SET_ALLOW_BROWSE: toggleDisabled, DID_SET_ALLOW_DIRECTORIES_ONLY: toggleDirectoryFilter, DID_SET_ALLOW_MULTIPLE: toggleAllowMultiple, DID_SET_ACCEPTED_FILE_TYPES: setAcceptedFileTypes, DID_SET_CAPTURE_METHOD: setCaptureMethod, DID_SET_REQUIRED: toggleRequired, }), }); var Key = { ENTER: 13, SPACE: 32, }; var create$b = function create(_ref) { var root = _ref.root, props = _ref.props; // create the label and link it to the file browser var label = createElement$1('label'); attr(label, 'for', 'filepond--browser-' + props.id); // use for labeling file input (aria-labelledby on file input) attr(label, 'id', 'filepond--drop-label-' + props.id); // handle keys root.ref.handleKeyDown = function(e) { var isActivationKey = e.keyCode === Key.ENTER || e.keyCode === Key.SPACE; if (!isActivationKey) return; // stops from triggering the element a second time e.preventDefault(); // click link (will then in turn activate file input) root.ref.label.click(); }; root.ref.handleClick = function(e) { var isLabelClick = e.target === label || label.contains(e.target); // don't want to click twice if (isLabelClick) return; // click link (will then in turn activate file input) root.ref.label.click(); }; // attach events label.addEventListener('keydown', root.ref.handleKeyDown); root.element.addEventListener('click', root.ref.handleClick); // update updateLabelValue(label, props.caption); // add! root.appendChild(label); root.ref.label = label; }; var updateLabelValue = function updateLabelValue(label, value) { label.innerHTML = value; var clickable = label.querySelector('.filepond--label-action'); if (clickable) { attr(clickable, 'tabindex', '0'); } return value; }; var dropLabel = createView({ name: 'drop-label', ignoreRect: true, create: create$b, destroy: function destroy(_ref2) { var root = _ref2.root; root.ref.label.addEventListener('keydown', root.ref.handleKeyDown); root.element.removeEventListener('click', root.ref.handleClick); }, write: createRoute({ DID_SET_LABEL_IDLE: function DID_SET_LABEL_IDLE(_ref3) { var root = _ref3.root, action = _ref3.action; updateLabelValue(root.ref.label, action.value); }, }), mixins: { styles: ['opacity', 'translateX', 'translateY'], animations: { opacity: { type: 'tween', duration: 150 }, translateX: 'spring', translateY: 'spring', }, }, }); var blob = createView({ name: 'drip-blob', ignoreRect: true, mixins: { styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'], animations: { scaleX: 'spring', scaleY: 'spring', translateX: 'spring', translateY: 'spring', opacity: { type: 'tween', duration: 250 }, }, }, }); var addBlob = function addBlob(_ref) { var root = _ref.root; var centerX = root.rect.element.width * 0.5; var centerY = root.rect.element.height * 0.5; root.ref.blob = root.appendChildView( root.createChildView(blob, { opacity: 0, scaleX: 2.5, scaleY: 2.5, translateX: centerX, translateY: centerY, }) ); }; var moveBlob = function moveBlob(_ref2) { var root = _ref2.root, action = _ref2.action; if (!root.ref.blob) { addBlob({ root: root }); return; } root.ref.blob.translateX = action.position.scopeLeft; root.ref.blob.translateY = action.position.scopeTop; root.ref.blob.scaleX = 1; root.ref.blob.scaleY = 1; root.ref.blob.opacity = 1; }; var hideBlob = function hideBlob(_ref3) { var root = _ref3.root; if (!root.ref.blob) { return; } root.ref.blob.opacity = 0; }; var explodeBlob = function explodeBlob(_ref4) { var root = _ref4.root; if (!root.ref.blob) { return; } root.ref.blob.scaleX = 2.5; root.ref.blob.scaleY = 2.5; root.ref.blob.opacity = 0; }; var write$7 = function write(_ref5) { var root = _ref5.root, props = _ref5.props, actions = _ref5.actions; route$4({ root: root, props: props, actions: actions }); var blob = root.ref.blob; if (actions.length === 0 && blob && blob.opacity === 0) { root.removeChildView(blob); root.ref.blob = null; } }; var route$4 = createRoute({ DID_DRAG: moveBlob, DID_DROP: explodeBlob, DID_END_DRAG: hideBlob, }); var drip = createView({ ignoreRect: true, ignoreRectUpdate: true, name: 'drip', write: write$7, }); var setInputFiles = function setInputFiles(element, files) { try { // Create a DataTransfer instance and add a newly created file var dataTransfer = new DataTransfer(); files.forEach(function(file) { if (file instanceof File) { dataTransfer.items.add(file); } else { dataTransfer.items.add( new File([file], file.name, { type: file.type, }) ); } }); // Assign the DataTransfer files list to the file input element.files = dataTransfer.files; } catch (err) { return false; } return true; }; var create$c = function create(_ref) { var root = _ref.root; root.ref.fields = {}; var legend = document.createElement('legend'); legend.textContent = 'Files'; root.element.appendChild(legend); }; var getField = function getField(root, id) { return root.ref.fields[id]; }; var syncFieldPositionsWithItems = function syncFieldPositionsWithItems(root) { root.query('GET_ACTIVE_ITEMS').forEach(function(item) { if (!root.ref.fields[item.id]) return; root.element.appendChild(root.ref.fields[item.id]); }); }; var didReorderItems = function didReorderItems(_ref2) { var root = _ref2.root; return syncFieldPositionsWithItems(root); }; var didAddItem = function didAddItem(_ref3) { var root = _ref3.root, action = _ref3.action; var fileItem = root.query('GET_ITEM', action.id); var isLocalFile = fileItem.origin === FileOrigin.LOCAL; var shouldUseFileInput = !isLocalFile && root.query('SHOULD_UPDATE_FILE_INPUT'); var dataContainer = createElement$1('input'); dataContainer.type = shouldUseFileInput ? 'file' : 'hidden'; dataContainer.name = root.query('GET_NAME'); root.ref.fields[action.id] = dataContainer; syncFieldPositionsWithItems(root); }; var didLoadItem$1 = function didLoadItem(_ref4) { var root = _ref4.root, action = _ref4.action; var field = getField(root, action.id); if (!field) return; // store server ref in hidden input if (action.serverFileReference !== null) field.value = action.serverFileReference; // store file item in file input if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return; var fileItem = root.query('GET_ITEM', action.id); setInputFiles(field, [fileItem.file]); }; var didPrepareOutput = function didPrepareOutput(_ref5) { var root = _ref5.root, action = _ref5.action; // this timeout pushes the handler after 'load' if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return; setTimeout(function() { var field = getField(root, action.id); if (!field) return; setInputFiles(field, [action.file]); }, 0); }; var didSetDisabled = function didSetDisabled(_ref6) { var root = _ref6.root; root.element.disabled = root.query('GET_DISABLED'); }; var didRemoveItem = function didRemoveItem(_ref7) { var root = _ref7.root, action = _ref7.action; var field = getField(root, action.id); if (!field) return; if (field.parentNode) field.parentNode.removeChild(field); delete root.ref.fields[action.id]; }; // only runs for server files. will refuse to update the value if the field // is a file field var didDefineValue = function didDefineValue(_ref8) { var root = _ref8.root, action = _ref8.action; var field = getField(root, action.id); if (!field) return; if (action.value === null) { // clear field value field.removeAttribute('value'); } else { // set field value if (field.type != 'file') { field.value = action.value; } } syncFieldPositionsWithItems(root); }; var write$8 = createRoute({ DID_SET_DISABLED: didSetDisabled, DID_ADD_ITEM: didAddItem, DID_LOAD_ITEM: didLoadItem$1, DID_REMOVE_ITEM: didRemoveItem, DID_DEFINE_VALUE: didDefineValue, DID_PREPARE_OUTPUT: didPrepareOutput, DID_REORDER_ITEMS: didReorderItems, DID_SORT_ITEMS: didReorderItems, }); var data = createView({ tag: 'fieldset', name: 'data', create: create$c, write: write$8, ignoreRect: true, }); var getRootNode = function getRootNode(element) { return 'getRootNode' in element ? element.getRootNode() : document; }; var images = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'tiff']; var text$1 = ['css', 'csv', 'html', 'txt']; var map = { zip: 'zip|compressed', epub: 'application/epub+zip', }; var guesstimateMimeType = function guesstimateMimeType() { var extension = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; extension = extension.toLowerCase(); if (images.includes(extension)) { return ( 'image/' + (extension === 'jpg' ? 'jpeg' : extension === 'svg' ? 'svg+xml' : extension) ); } if (text$1.includes(extension)) { return 'text/' + extension; } return map[extension] || ''; }; var requestDataTransferItems = function requestDataTransferItems(dataTransfer) { return new Promise(function(resolve, reject) { // try to get links from transfer, if found we'll exit immediately (unless a file is in the dataTransfer as well, this is because Firefox could represent the file as a URL and a file object at the same time) var links = getLinks(dataTransfer); if (links.length && !hasFiles(dataTransfer)) { return resolve(links); } // try to get files from the transfer getFiles(dataTransfer).then(resolve); }); }; /** * Test if datatransfer has files */ var hasFiles = function hasFiles(dataTransfer) { if (dataTransfer.files) return dataTransfer.files.length > 0; return false; }; /** * Extracts files from a DataTransfer object */ var getFiles = function getFiles(dataTransfer) { return new Promise(function(resolve, reject) { // get the transfer items as promises var promisedFiles = (dataTransfer.items ? Array.from(dataTransfer.items) : []) // only keep file system items (files and directories) .filter(function(item) { return isFileSystemItem(item); }) // map each item to promise .map(function(item) { return getFilesFromItem(item); }); // if is empty, see if we can extract some info from the files property as a fallback if (!promisedFiles.length) { // TODO: test for directories (should not be allowed) // Use FileReader, problem is that the files property gets lost in the process resolve(dataTransfer.files ? Array.from(dataTransfer.files) : []); return; } // done! Promise.all(promisedFiles) .then(function(returnedFileGroups) { // flatten groups var files = []; returnedFileGroups.forEach(function(group) { files.push.apply(files, group); }); // done (filter out empty files)! resolve( files .filter(function(file) { return file; }) .map(function(file) { if (!file._relativePath) file._relativePath = file.webkitRelativePath; return file; }) ); }) .catch(console.error); }); }; var isFileSystemItem = function isFileSystemItem(item) { if (isEntry(item)) { var entry = getAsEntry(item); if (entry) { return entry.isFile || entry.isDirectory; } } return item.kind === 'file'; }; var getFilesFromItem = function getFilesFromItem(item) { return new Promise(function(resolve, reject) { if (isDirectoryEntry(item)) { getFilesInDirectory(getAsEntry(item)) .then(resolve) .catch(reject); return; } resolve([item.getAsFile()]); }); }; var getFilesInDirectory = function getFilesInDirectory(entry) { return new Promise(function(resolve, reject) { var files = []; // the total entries to read var dirCounter = 0; var fileCounter = 0; var resolveIfDone = function resolveIfDone() { if (fileCounter === 0 && dirCounter === 0) { resolve(files); } }; // the recursive function var readEntries = function readEntries(dirEntry) { dirCounter++; var directoryReader = dirEntry.createReader(); // directories are returned in batches, we need to process all batches before we're done var readBatch = function readBatch() { directoryReader.readEntries(function(entries) { if (entries.length === 0) { dirCounter--; resolveIfDone(); return; } entries.forEach(function(entry) { // recursively read more directories if (entry.isDirectory) { readEntries(entry); } else { // read as file fileCounter++; entry.file(function(file) { var correctedFile = correctMissingFileType(file); if (entry.fullPath) correctedFile._relativePath = entry.fullPath; files.push(correctedFile); fileCounter--; resolveIfDone(); }); } }); // try to get next batch of files readBatch(); }, reject); }; // read first batch of files readBatch(); }; // go! readEntries(entry); }); }; var correctMissingFileType = function correctMissingFileType(file) { if (file.type.length) return file; var date = file.lastModifiedDate; var name = file.name; var type = guesstimateMimeType(getExtensionFromFilename(file.name)); if (!type.length) return file; file = file.slice(0, file.size, type); file.name = name; file.lastModifiedDate = date; return file; }; var isDirectoryEntry = function isDirectoryEntry(item) { return isEntry(item) && (getAsEntry(item) || {}).isDirectory; }; var isEntry = function isEntry(item) { return 'webkitGetAsEntry' in item; }; var getAsEntry = function getAsEntry(item) { return item.webkitGetAsEntry(); }; /** * Extracts links from a DataTransfer object */ var getLinks = function getLinks(dataTransfer) { var links = []; try { // look in meta data property links = getLinksFromTransferMetaData(dataTransfer); if (links.length) { return links; } links = getLinksFromTransferURLData(dataTransfer); } catch (e) { // nope nope nope (probably IE trouble) } return links; }; var getLinksFromTransferURLData = function getLinksFromTransferURLData(dataTransfer) { var data = dataTransfer.getData('url'); if (typeof data === 'string' && data.length) { return [data]; } return []; }; var getLinksFromTransferMetaData = function getLinksFromTransferMetaData(dataTransfer) { var data = dataTransfer.getData('text/html'); if (typeof data === 'string' && data.length) { var matches = data.match(/src\s*=\s*"(.+?)"/); if (matches) { return [matches[1]]; } } return []; }; var dragNDropObservers = []; var eventPosition = function eventPosition(e) { return { pageLeft: e.pageX, pageTop: e.pageY, scopeLeft: e.offsetX || e.layerX, scopeTop: e.offsetY || e.layerY, }; }; var createDragNDropClient = function createDragNDropClient( element, scopeToObserve, filterElement ) { var observer = getDragNDropObserver(scopeToObserve); var client = { element: element, filterElement: filterElement, state: null, ondrop: function ondrop() {}, onenter: function onenter() {}, ondrag: function ondrag() {}, onexit: function onexit() {}, onload: function onload() {}, allowdrop: function allowdrop() {}, }; client.destroy = observer.addListener(client); return client; }; var getDragNDropObserver = function getDragNDropObserver(element) { // see if already exists, if so, return var observer = dragNDropObservers.find(function(item) { return item.element === element; }); if (observer) { return observer; } // create new observer, does not yet exist for this element var newObserver = createDragNDropObserver(element); dragNDropObservers.push(newObserver); return newObserver; }; var createDragNDropObserver = function createDragNDropObserver(element) { var clients = []; var routes = { dragenter: dragenter, dragover: dragover, dragleave: dragleave, drop: drop, }; var handlers = {}; forin(routes, function(event, createHandler) { handlers[event] = createHandler(element, clients); element.addEventListener(event, handlers[event], false); }); var observer = { element: element, addListener: function addListener(client) { // add as client clients.push(client); // return removeListener function return function() { // remove client clients.splice(clients.indexOf(client), 1); // if no more clients, clean up observer if (clients.length === 0) { dragNDropObservers.splice(dragNDropObservers.indexOf(observer), 1); forin(routes, function(event) { element.removeEventListener(event, handlers[event], false); }); } }; }, }; return observer; }; var elementFromPoint = function elementFromPoint(root, point) { if (!('elementFromPoint' in root)) { root = document; } return root.elementFromPoint(point.x, point.y); }; var isEventTarget = function isEventTarget(e, target) { // get root var root = getRootNode(target); // get element at position // if root is not actual shadow DOM and does not have elementFromPoint method, use the one on document var elementAtPosition = elementFromPoint(root, { x: e.pageX - window.pageXOffset, y: e.pageY - window.pageYOffset, }); // test if target is the element or if one of its children is return elementAtPosition === target || target.contains(elementAtPosition); }; var initialTarget = null; var setDropEffect = function setDropEffect(dataTransfer, effect) { // is in try catch as IE11 will throw error if not try { dataTransfer.dropEffect = effect; } catch (e) {} }; var dragenter = function dragenter(root, clients) { return function(e) { e.preventDefault(); initialTarget = e.target; clients.forEach(function(client) { var element = client.element, onenter = client.onenter; if (isEventTarget(e, element)) { client.state = 'enter'; // fire enter event onenter(eventPosition(e)); } }); }; }; var dragover = function dragover(root, clients) { return function(e) { e.preventDefault(); var dataTransfer = e.dataTransfer; requestDataTransferItems(dataTransfer).then(function(items) { var overDropTarget = false; clients.some(function(client) { var filterElement = client.filterElement, element = client.element, onenter = client.onenter, onexit = client.onexit, ondrag = client.ondrag, allowdrop = client.allowdrop; // by default we can drop setDropEffect(dataTransfer, 'copy'); // allow transfer of these items var allowsTransfer = allowdrop(items); // only used when can be dropped on page if (!allowsTransfer) { setDropEffect(dataTransfer, 'none'); return; } // targetting this client if (isEventTarget(e, element)) { overDropTarget = true; // had no previous state, means we are entering this client if (client.state === null) { client.state = 'enter'; onenter(eventPosition(e)); return; } // now over element (no matter if it allows the drop or not) client.state = 'over'; // needs to allow transfer if (filterElement && !allowsTransfer) { setDropEffect(dataTransfer, 'none'); return; } // dragging ondrag(eventPosition(e)); } else { // should be over an element to drop if (filterElement && !overDropTarget) { setDropEffect(dataTransfer, 'none'); } // might have just left this client? if (client.state) { client.state = null; onexit(eventPosition(e)); } } }); }); }; }; var drop = function drop(root, clients) { return function(e) { e.preventDefault(); var dataTransfer = e.dataTransfer; requestDataTransferItems(dataTransfer).then(function(items) { clients.forEach(function(client) { var filterElement = client.filterElement, element = client.element, ondrop = client.ondrop, onexit = client.onexit, allowdrop = client.allowdrop; client.state = null; // if we're filtering on element we need to be over the element to drop if (filterElement && !isEventTarget(e, element)) return; // no transfer for this client if (!allowdrop(items)) return onexit(eventPosition(e)); // we can drop these items on this client ondrop(eventPosition(e), items); }); }); }; }; var dragleave = function dragleave(root, clients) { return function(e) { if (initialTarget !== e.target) { return; } clients.forEach(function(client) { var onexit = client.onexit; client.state = null; onexit(eventPosition(e)); }); }; }; var createHopper = function createHopper(scope, validateItems, options) { // is now hopper scope scope.classList.add('filepond--hopper'); // shortcuts var catchesDropsOnPage = options.catchesDropsOnPage, requiresDropOnElement = options.requiresDropOnElement, _options$filterItems = options.filterItems, filterItems = _options$filterItems === void 0 ? function(items) { return items; } : _options$filterItems; // create a dnd client var client = createDragNDropClient( scope, catchesDropsOnPage ? document.documentElement : scope, requiresDropOnElement ); // current client state var lastState = ''; var currentState = ''; // determines if a file may be dropped client.allowdrop = function(items) { // TODO: if we can, throw error to indicate the items cannot by dropped return validateItems(filterItems(items)); }; client.ondrop = function(position, items) { var filteredItems = filterItems(items); if (!validateItems(filteredItems)) { api.ondragend(position); return; } currentState = 'drag-drop'; api.onload(filteredItems, position); }; client.ondrag = function(position) { api.ondrag(position); }; client.onenter = function(position) { currentState = 'drag-over'; api.ondragstart(position); }; client.onexit = function(position) { currentState = 'drag-exit'; api.ondragend(position); }; var api = { updateHopperState: function updateHopperState() { if (lastState !== currentState) { scope.dataset.hopperState = currentState; lastState = currentState; } }, onload: function onload() {}, ondragstart: function ondragstart() {}, ondrag: function ondrag() {}, ondragend: function ondragend() {}, destroy: function destroy() { // destroy client client.destroy(); }, }; return api; }; var listening = false; var listeners$1 = []; var handlePaste = function handlePaste(e) { // if is pasting in input or textarea and the target is outside of a filepond scope, ignore var activeEl = document.activeElement; var isActiveElementEditable = activeEl && (/textarea|input/i.test(activeEl.nodeName) || activeEl.getAttribute('contenteditable') === 'true' || activeEl.getAttribute('contenteditable') === ''); if (isActiveElementEditable) { // test textarea or input is contained in filepond root var inScope = false; var element = activeEl; while (element !== document.body) { if (element.classList.contains('filepond--root')) { inScope = true; break; } element = element.parentNode; } if (!inScope) return; } requestDataTransferItems(e.clipboardData).then(function(files) { // no files received if (!files.length) { return; } // notify listeners of received files listeners$1.forEach(function(listener) { return listener(files); }); }); }; var listen = function listen(cb) { // can't add twice if (listeners$1.includes(cb)) { return; } // add initial listener listeners$1.push(cb); // setup paste listener for entire page if (listening) { return; } listening = true; document.addEventListener('paste', handlePaste); }; var unlisten = function unlisten(listener) { arrayRemove(listeners$1, listeners$1.indexOf(listener)); // clean up if (listeners$1.length === 0) { document.removeEventListener('paste', handlePaste); listening = false; } }; var createPaster = function createPaster() { var cb = function cb(files) { api.onload(files); }; var api = { destroy: function destroy() { unlisten(cb); }, onload: function onload() {}, }; listen(cb); return api; }; /** * Creates the file view */ var create$d = function create(_ref) { var root = _ref.root, props = _ref.props; root.element.id = 'filepond--assistant-' + props.id; attr(root.element, 'role', 'alert'); attr(root.element, 'aria-live', 'polite'); attr(root.element, 'aria-relevant', 'additions'); }; var addFilesNotificationTimeout = null; var notificationClearTimeout = null; var filenames = []; var assist = function assist(root, message) { root.element.textContent = message; }; var clear$1 = function clear(root) { root.element.textContent = ''; }; var listModified = function listModified(root, filename, label) { var total = root.query('GET_TOTAL_ITEMS'); assist( root, label + ' ' + filename + ', ' + total + ' ' + (total === 1 ? root.query('GET_LABEL_FILE_COUNT_SINGULAR') : root.query('GET_LABEL_FILE_COUNT_PLURAL')) ); // clear group after set amount of time so the status is not read twice clearTimeout(notificationClearTimeout); notificationClearTimeout = setTimeout(function() { clear$1(root); }, 1500); }; var isUsingFilePond = function isUsingFilePond(root) { return root.element.parentNode.contains(document.activeElement); }; var itemAdded = function itemAdded(_ref2) { var root = _ref2.root, action = _ref2.action; if (!isUsingFilePond(root)) { return; } root.element.textContent = ''; var item = root.query('GET_ITEM', action.id); filenames.push(item.filename); clearTimeout(addFilesNotificationTimeout); addFilesNotificationTimeout = setTimeout(function() { listModified(root, filenames.join(', '), root.query('GET_LABEL_FILE_ADDED')); filenames.length = 0; }, 750); }; var itemRemoved = function itemRemoved(_ref3) { var root = _ref3.root, action = _ref3.action; if (!isUsingFilePond(root)) { return; } var item = action.item; listModified(root, item.filename, root.query('GET_LABEL_FILE_REMOVED')); }; var itemProcessed = function itemProcessed(_ref4) { var root = _ref4.root, action = _ref4.action; // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file var item = root.query('GET_ITEM', action.id); var filename = item.filename; var label = root.query('GET_LABEL_FILE_PROCESSING_COMPLETE'); assist(root, filename + ' ' + label); }; var itemProcessedUndo = function itemProcessedUndo(_ref5) { var root = _ref5.root, action = _ref5.action; var item = root.query('GET_ITEM', action.id); var filename = item.filename; var label = root.query('GET_LABEL_FILE_PROCESSING_ABORTED'); assist(root, filename + ' ' + label); }; var itemError = function itemError(_ref6) { var root = _ref6.root, action = _ref6.action; var item = root.query('GET_ITEM', action.id); var filename = item.filename; // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file assist(root, action.status.main + ' ' + filename + ' ' + action.status.sub); }; var assistant = createView({ create: create$d, ignoreRect: true, ignoreRectUpdate: true, write: createRoute({ DID_LOAD_ITEM: itemAdded, DID_REMOVE_ITEM: itemRemoved, DID_COMPLETE_ITEM_PROCESSING: itemProcessed, DID_ABORT_ITEM_PROCESSING: itemProcessedUndo, DID_REVERT_ITEM_PROCESSING: itemProcessedUndo, DID_THROW_ITEM_REMOVE_ERROR: itemError, DID_THROW_ITEM_LOAD_ERROR: itemError, DID_THROW_ITEM_INVALID: itemError, DID_THROW_ITEM_PROCESSING_ERROR: itemError, }), tag: 'span', name: 'assistant', }); var toCamels = function toCamels(string) { var separator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '-'; return string.replace(new RegExp(separator + '.', 'g'), function(sub) { return sub.charAt(1).toUpperCase(); }); }; var debounce = function debounce(func) { var interval = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 16; var immidiateOnly = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; var last = Date.now(); var timeout = null; return function() { for ( var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++ ) { args[_key] = arguments[_key]; } clearTimeout(timeout); var dist = Date.now() - last; var fn = function fn() { last = Date.now(); func.apply(void 0, args); }; if (dist < interval) { // we need to delay by the difference between interval and dist // for example: if distance is 10 ms and interval is 16 ms, // we need to wait an additional 6ms before calling the function) if (!immidiateOnly) { timeout = setTimeout(fn, interval - dist); } } else { // go! fn(); } }; }; var MAX_FILES_LIMIT = 1000000; var prevent = function prevent(e) { return e.preventDefault(); }; var create$e = function create(_ref) { var root = _ref.root, props = _ref.props; // Add id var id = root.query('GET_ID'); if (id) { root.element.id = id; } // Add className var className = root.query('GET_CLASS_NAME'); if (className) { className .split(' ') .filter(function(name) { return name.length; }) .forEach(function(name) { root.element.classList.add(name); }); } // Field label root.ref.label = root.appendChildView( root.createChildView( dropLabel, Object.assign({}, props, { translateY: null, caption: root.query('GET_LABEL_IDLE'), }) ) ); // List of items root.ref.list = root.appendChildView( root.createChildView(listScroller, { translateY: null }) ); // Background panel root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'panel-root' })); // Assistant notifies assistive tech when content changes root.ref.assistant = root.appendChildView( root.createChildView(assistant, Object.assign({}, props)) ); // Data root.ref.data = root.appendChildView(root.createChildView(data, Object.assign({}, props))); // Measure (tests if fixed height was set) // DOCTYPE needs to be set for this to work root.ref.measure = createElement$1('div'); root.ref.measure.style.height = '100%'; root.element.appendChild(root.ref.measure); // information on the root height or fixed height status root.ref.bounds = null; // apply initial style properties root.query('GET_STYLES') .filter(function(style) { return !isEmpty(style.value); }) .map(function(_ref2) { var name = _ref2.name, value = _ref2.value; root.element.dataset[name] = value; }); // determine if width changed root.ref.widthPrevious = null; root.ref.widthUpdated = debounce(function() { root.ref.updateHistory = []; root.dispatch('DID_RESIZE_ROOT'); }, 250); // history of updates root.ref.previousAspectRatio = null; root.ref.updateHistory = []; // prevent scrolling and zooming on iOS (only if supports pointer events, for then we can enable reorder) var canHover = window.matchMedia('(pointer: fine) and (hover: hover)').matches; var hasPointerEvents = 'PointerEvent' in window; if (root.query('GET_ALLOW_REORDER') && hasPointerEvents && !canHover) { root.element.addEventListener('touchmove', prevent, { passive: false }); root.element.addEventListener('gesturestart', prevent); } // add credits var credits = root.query('GET_CREDITS'); var hasCredits = credits.length === 2; if (hasCredits) { var frag = document.createElement('a'); frag.className = 'filepond--credits'; frag.href = credits[0]; frag.tabIndex = -1; frag.target = '_blank'; frag.rel = 'noopener noreferrer nofollow'; frag.textContent = credits[1]; root.element.appendChild(frag); root.ref.credits = frag; } }; var write$9 = function write(_ref3) { var root = _ref3.root, props = _ref3.props, actions = _ref3.actions; // route actions route$5({ root: root, props: props, actions: actions }); // apply style properties actions .filter(function(action) { return /^DID_SET_STYLE_/.test(action.type); }) .filter(function(action) { return !isEmpty(action.data.value); }) .map(function(_ref4) { var type = _ref4.type, data = _ref4.data; var name = toCamels(type.substring(8).toLowerCase(), '_'); root.element.dataset[name] = data.value; root.invalidateLayout(); }); if (root.rect.element.hidden) return; if (root.rect.element.width !== root.ref.widthPrevious) { root.ref.widthPrevious = root.rect.element.width; root.ref.widthUpdated(); } // get box bounds, we do this only once var bounds = root.ref.bounds; if (!bounds) { bounds = root.ref.bounds = calculateRootBoundingBoxHeight(root); // destroy measure element root.element.removeChild(root.ref.measure); root.ref.measure = null; } // get quick references to various high level parts of the upload tool var _root$ref = root.ref, hopper = _root$ref.hopper, label = _root$ref.label, list = _root$ref.list, panel = _root$ref.panel; // sets correct state to hopper scope if (hopper) { hopper.updateHopperState(); } // bool to indicate if we're full or not var aspectRatio = root.query('GET_PANEL_ASPECT_RATIO'); var isMultiItem = root.query('GET_ALLOW_MULTIPLE'); var totalItems = root.query('GET_TOTAL_ITEMS'); var maxItems = isMultiItem ? root.query('GET_MAX_FILES') || MAX_FILES_LIMIT : 1; var atMaxCapacity = totalItems === maxItems; // action used to add item var addAction = actions.find(function(action) { return action.type === 'DID_ADD_ITEM'; }); // if reached max capacity and we've just reached it if (atMaxCapacity && addAction) { // get interaction type var interactionMethod = addAction.data.interactionMethod; // hide label label.opacity = 0; if (isMultiItem) { label.translateY = -40; } else { if (interactionMethod === InteractionMethod.API) { label.translateX = 40; } else if (interactionMethod === InteractionMethod.BROWSE) { label.translateY = 40; } else { label.translateY = 30; } } } else if (!atMaxCapacity) { label.opacity = 1; label.translateX = 0; label.translateY = 0; } var listItemMargin = calculateListItemMargin(root); var listHeight = calculateListHeight(root); var labelHeight = label.rect.element.height; var currentLabelHeight = !isMultiItem || atMaxCapacity ? 0 : labelHeight; var listMarginTop = atMaxCapacity ? list.rect.element.marginTop : 0; var listMarginBottom = totalItems === 0 ? 0 : list.rect.element.marginBottom; var visualHeight = currentLabelHeight + listMarginTop + listHeight.visual + listMarginBottom; var boundsHeight = currentLabelHeight + listMarginTop + listHeight.bounds + listMarginBottom; // link list to label bottom position list.translateY = Math.max(0, currentLabelHeight - list.rect.element.marginTop) - listItemMargin.top; if (aspectRatio) { // fixed aspect ratio // calculate height based on width var width = root.rect.element.width; var height = width * aspectRatio; // clear history if aspect ratio has changed if (aspectRatio !== root.ref.previousAspectRatio) { root.ref.previousAspectRatio = aspectRatio; root.ref.updateHistory = []; } // remember this width var history = root.ref.updateHistory; history.push(width); var MAX_BOUNCES = 2; if (history.length > MAX_BOUNCES * 2) { var l = history.length; var bottom = l - 10; var bounces = 0; for (var i = l; i >= bottom; i--) { if (history[i] === history[i - 2]) { bounces++; } if (bounces >= MAX_BOUNCES) { // dont adjust height return; } } } // fix height of panel so it adheres to aspect ratio panel.scalable = false; panel.height = height; // available height for list var listAvailableHeight = // the height of the panel minus the label height height - currentLabelHeight - // the room we leave open between the end of the list and the panel bottom (listMarginBottom - listItemMargin.bottom) - // if we're full we need to leave some room between the top of the panel and the list (atMaxCapacity ? listMarginTop : 0); if (listHeight.visual > listAvailableHeight) { list.overflow = listAvailableHeight; } else { list.overflow = null; } // set container bounds (so pushes siblings downwards) root.height = height; } else if (bounds.fixedHeight) { // fixed height // fix height of panel panel.scalable = false; // available height for list var _listAvailableHeight = // the height of the panel minus the label height bounds.fixedHeight - currentLabelHeight - // the room we leave open between the end of the list and the panel bottom (listMarginBottom - listItemMargin.bottom) - // if we're full we need to leave some room between the top of the panel and the list (atMaxCapacity ? listMarginTop : 0); // set list height if (listHeight.visual > _listAvailableHeight) { list.overflow = _listAvailableHeight; } else { list.overflow = null; } // no need to set container bounds as these are handles by CSS fixed height } else if (bounds.cappedHeight) { // max-height // not a fixed height panel var isCappedHeight = visualHeight >= bounds.cappedHeight; var panelHeight = Math.min(bounds.cappedHeight, visualHeight); panel.scalable = true; panel.height = isCappedHeight ? panelHeight : panelHeight - listItemMargin.top - listItemMargin.bottom; // available height for list var _listAvailableHeight2 = // the height of the panel minus the label height panelHeight - currentLabelHeight - // the room we leave open between the end of the list and the panel bottom (listMarginBottom - listItemMargin.bottom) - // if we're full we need to leave some room between the top of the panel and the list (atMaxCapacity ? listMarginTop : 0); // set list height (if is overflowing) if (visualHeight > bounds.cappedHeight && listHeight.visual > _listAvailableHeight2) { list.overflow = _listAvailableHeight2; } else { list.overflow = null; } // set container bounds (so pushes siblings downwards) root.height = Math.min( bounds.cappedHeight, boundsHeight - listItemMargin.top - listItemMargin.bottom ); } else { // flexible height // not a fixed height panel var itemMargin = totalItems > 0 ? listItemMargin.top + listItemMargin.bottom : 0; panel.scalable = true; panel.height = Math.max(labelHeight, visualHeight - itemMargin); // set container bounds (so pushes siblings downwards) root.height = Math.max(labelHeight, boundsHeight - itemMargin); } // move credits to bottom if (root.ref.credits && panel.heightCurrent) root.ref.credits.style.transform = 'translateY(' + panel.heightCurrent + 'px)'; }; var calculateListItemMargin = function calculateListItemMargin(root) { var item = root.ref.list.childViews[0].childViews[0]; return item ? { top: item.rect.element.marginTop, bottom: item.rect.element.marginBottom, } : { top: 0, bottom: 0, }; }; var calculateListHeight = function calculateListHeight(root) { var visual = 0; var bounds = 0; // get file list reference var scrollList = root.ref.list; var itemList = scrollList.childViews[0]; var visibleChildren = itemList.childViews.filter(function(child) { return child.rect.element.height; }); var children = root .query('GET_ACTIVE_ITEMS') .map(function(item) { return visibleChildren.find(function(child) { return child.id === item.id; }); }) .filter(function(item) { return item; }); // no children, done! if (children.length === 0) return { visual: visual, bounds: bounds }; var horizontalSpace = itemList.rect.element.width; var dragIndex = getItemIndexByPosition(itemList, children, scrollList.dragCoordinates); var childRect = children[0].rect.element; var itemVerticalMargin = childRect.marginTop + childRect.marginBottom; var itemHorizontalMargin = childRect.marginLeft + childRect.marginRight; var itemWidth = childRect.width + itemHorizontalMargin; var itemHeight = childRect.height + itemVerticalMargin; var newItem = typeof dragIndex !== 'undefined' && dragIndex >= 0 ? 1 : 0; var removedItem = children.find(function(child) { return child.markedForRemoval && child.opacity < 0.45; }) ? -1 : 0; var verticalItemCount = children.length + newItem + removedItem; var itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth); // stack if (itemsPerRow === 1) { children.forEach(function(item) { var height = item.rect.element.height + itemVerticalMargin; bounds += height; visual += height * item.opacity; }); } // grid else { bounds = Math.ceil(verticalItemCount / itemsPerRow) * itemHeight; visual = bounds; } return { visual: visual, bounds: bounds }; }; var calculateRootBoundingBoxHeight = function calculateRootBoundingBoxHeight(root) { var height = root.ref.measureHeight || null; var cappedHeight = parseInt(root.style.maxHeight, 10) || null; var fixedHeight = height === 0 ? null : height; return { cappedHeight: cappedHeight, fixedHeight: fixedHeight, }; }; var exceedsMaxFiles = function exceedsMaxFiles(root, items) { var allowReplace = root.query('GET_ALLOW_REPLACE'); var allowMultiple = root.query('GET_ALLOW_MULTIPLE'); var totalItems = root.query('GET_TOTAL_ITEMS'); var maxItems = root.query('GET_MAX_FILES'); // total amount of items being dragged var totalBrowseItems = items.length; // if does not allow multiple items and dragging more than one item if (!allowMultiple && totalBrowseItems > 1) { root.dispatch('DID_THROW_MAX_FILES', { source: items, error: createResponse('warning', 0, 'Max files'), }); return true; } // limit max items to one if not allowed to drop multiple items maxItems = allowMultiple ? maxItems : 1; if (!allowMultiple && allowReplace) { // There is only one item, so there is room to replace or add an item return false; } // no more room? var hasMaxItems = isInt(maxItems); if (hasMaxItems && totalItems + totalBrowseItems > maxItems) { root.dispatch('DID_THROW_MAX_FILES', { source: items, error: createResponse('warning', 0, 'Max files'), }); return true; } return false; }; var getDragIndex = function getDragIndex(list, children, position) { var itemList = list.childViews[0]; return getItemIndexByPosition(itemList, children, { left: position.scopeLeft - itemList.rect.element.left, top: position.scopeTop - (list.rect.outer.top + list.rect.element.marginTop + list.rect.element.scrollTop), }); }; /** * Enable or disable file drop functionality */ var toggleDrop = function toggleDrop(root) { var isAllowed = root.query('GET_ALLOW_DROP'); var isDisabled = root.query('GET_DISABLED'); var enabled = isAllowed && !isDisabled; if (enabled && !root.ref.hopper) { var hopper = createHopper( root.element, function(items) { // allow quick validation of dropped items var beforeDropFile = root.query('GET_BEFORE_DROP_FILE') || function() { return true; }; // all items should be validated by all filters as valid var dropValidation = root.query('GET_DROP_VALIDATION'); return dropValidation ? items.every(function(item) { return ( applyFilters('ALLOW_HOPPER_ITEM', item, { query: root.query, }).every(function(result) { return result === true; }) && beforeDropFile(item) ); }) : true; }, { filterItems: function filterItems(items) { var ignoredFiles = root.query('GET_IGNORED_FILES'); return items.filter(function(item) { if (isFile(item)) { return !ignoredFiles.includes(item.name.toLowerCase()); } return true; }); }, catchesDropsOnPage: root.query('GET_DROP_ON_PAGE'), requiresDropOnElement: root.query('GET_DROP_ON_ELEMENT'), } ); hopper.onload = function(items, position) { // get item children elements and sort based on list sort var list = root.ref.list.childViews[0]; var visibleChildren = list.childViews.filter(function(child) { return child.rect.element.height; }); var children = root .query('GET_ACTIVE_ITEMS') .map(function(item) { return visibleChildren.find(function(child) { return child.id === item.id; }); }) .filter(function(item) { return item; }); applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(function( queue ) { // these files don't fit so stop here if (exceedsMaxFiles(root, queue)) return false; // go root.dispatch('ADD_ITEMS', { items: queue, index: getDragIndex(root.ref.list, children, position), interactionMethod: InteractionMethod.DROP, }); }); root.dispatch('DID_DROP', { position: position }); root.dispatch('DID_END_DRAG', { position: position }); }; hopper.ondragstart = function(position) { root.dispatch('DID_START_DRAG', { position: position }); }; hopper.ondrag = debounce(function(position) { root.dispatch('DID_DRAG', { position: position }); }); hopper.ondragend = function(position) { root.dispatch('DID_END_DRAG', { position: position }); }; root.ref.hopper = hopper; root.ref.drip = root.appendChildView(root.createChildView(drip)); } else if (!enabled && root.ref.hopper) { root.ref.hopper.destroy(); root.ref.hopper = null; root.removeChildView(root.ref.drip); } }; /** * Enable or disable browse functionality */ var toggleBrowse = function toggleBrowse(root, props) { var isAllowed = root.query('GET_ALLOW_BROWSE'); var isDisabled = root.query('GET_DISABLED'); var enabled = isAllowed && !isDisabled; if (enabled && !root.ref.browser) { root.ref.browser = root.appendChildView( root.createChildView( browser, Object.assign({}, props, { onload: function onload(items) { applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch, }).then(function(queue) { // these files don't fit so stop here if (exceedsMaxFiles(root, queue)) return false; // add items! root.dispatch('ADD_ITEMS', { items: queue, index: -1, interactionMethod: InteractionMethod.BROWSE, }); }); }, }) ), 0 ); } else if (!enabled && root.ref.browser) { root.removeChildView(root.ref.browser); root.ref.browser = null; } }; /** * Enable or disable paste functionality */ var togglePaste = function togglePaste(root) { var isAllowed = root.query('GET_ALLOW_PASTE'); var isDisabled = root.query('GET_DISABLED'); var enabled = isAllowed && !isDisabled; if (enabled && !root.ref.paster) { root.ref.paster = createPaster(); root.ref.paster.onload = function(items) { applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(function( queue ) { // these files don't fit so stop here if (exceedsMaxFiles(root, queue)) return false; // add items! root.dispatch('ADD_ITEMS', { items: queue, index: -1, interactionMethod: InteractionMethod.PASTE, }); }); }; } else if (!enabled && root.ref.paster) { root.ref.paster.destroy(); root.ref.paster = null; } }; /** * Route actions */ var route$5 = createRoute({ DID_SET_ALLOW_BROWSE: function DID_SET_ALLOW_BROWSE(_ref5) { var root = _ref5.root, props = _ref5.props; toggleBrowse(root, props); }, DID_SET_ALLOW_DROP: function DID_SET_ALLOW_DROP(_ref6) { var root = _ref6.root; toggleDrop(root); }, DID_SET_ALLOW_PASTE: function DID_SET_ALLOW_PASTE(_ref7) { var root = _ref7.root; togglePaste(root); }, DID_SET_DISABLED: function DID_SET_DISABLED(_ref8) { var root = _ref8.root, props = _ref8.props; toggleDrop(root); togglePaste(root); toggleBrowse(root, props); var isDisabled = root.query('GET_DISABLED'); if (isDisabled) { root.element.dataset.disabled = 'disabled'; } else { // delete root.element.dataset.disabled; <= this does not work on iOS 10 root.element.removeAttribute('data-disabled'); } }, }); var root = createView({ name: 'root', read: function read(_ref9) { var root = _ref9.root; if (root.ref.measure) { root.ref.measureHeight = root.ref.measure.offsetHeight; } }, create: create$e, write: write$9, destroy: function destroy(_ref10) { var root = _ref10.root; if (root.ref.paster) { root.ref.paster.destroy(); } if (root.ref.hopper) { root.ref.hopper.destroy(); } root.element.removeEventListener('touchmove', prevent); root.element.removeEventListener('gesturestart', prevent); }, mixins: { styles: ['height'], }, }); // creates the app var createApp = function createApp() { var initialOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; // let element var originalElement = null; // get default options var defaultOptions = getOptions(); // create the data store, this will contain all our app info var store = createStore( // initial state (should be serializable) createInitialState(defaultOptions), // queries [queries, createOptionQueries(defaultOptions)], // action handlers [actions, createOptionActions(defaultOptions)] ); // set initial options store.dispatch('SET_OPTIONS', { options: initialOptions }); // kick thread if visibility changes var visibilityHandler = function visibilityHandler() { if (document.hidden) return; store.dispatch('KICK'); }; document.addEventListener('visibilitychange', visibilityHandler); // re-render on window resize start and finish var resizeDoneTimer = null; var isResizing = false; var isResizingHorizontally = false; var initialWindowWidth = null; var currentWindowWidth = null; var resizeHandler = function resizeHandler() { if (!isResizing) { isResizing = true; } clearTimeout(resizeDoneTimer); resizeDoneTimer = setTimeout(function() { isResizing = false; initialWindowWidth = null; currentWindowWidth = null; if (isResizingHorizontally) { isResizingHorizontally = false; store.dispatch('DID_STOP_RESIZE'); } }, 500); }; window.addEventListener('resize', resizeHandler); // render initial view var view = root(store, { id: getUniqueId() }); // // PRIVATE API ------------------------------------------------------------------------------------- // var isResting = false; var isHidden = false; var readWriteApi = { // necessary for update loop /** * Reads from dom (never call manually) * @private */ _read: function _read() { // test if we're resizing horizontally // TODO: see if we can optimize this by measuring root rect if (isResizing) { currentWindowWidth = window.innerWidth; if (!initialWindowWidth) { initialWindowWidth = currentWindowWidth; } if (!isResizingHorizontally && currentWindowWidth !== initialWindowWidth) { store.dispatch('DID_START_RESIZE'); isResizingHorizontally = true; } } if (isHidden && isResting) { // test if is no longer hidden isResting = view.element.offsetParent === null; } // if resting, no need to read as numbers will still all be correct if (isResting) return; // read view data view._read(); // if is hidden we need to know so we exit rest mode when revealed isHidden = view.rect.element.hidden; }, /** * Writes to dom (never call manually) * @private */ _write: function _write(ts) { // get all actions from store var actions = store .processActionQueue() // filter out set actions (these will automatically trigger DID_SET) .filter(function(action) { return !/^SET_/.test(action.type); }); // if was idling and no actions stop here if (isResting && !actions.length) return; // some actions might trigger events routeActionsToEvents(actions); // update the view isResting = view._write(ts, actions, isResizingHorizontally); // will clean up all archived items removeReleasedItems(store.query('GET_ITEMS')); // now idling if (isResting) { store.processDispatchQueue(); } }, }; // // EXPOSE EVENTS ------------------------------------------------------------------------------------- // var createEvent = function createEvent(name) { return function(data) { // create default event var event = { type: name, }; // no data to add if (!data) { return event; } // copy relevant props if (data.hasOwnProperty('error')) { event.error = data.error ? Object.assign({}, data.error) : null; } if (data.status) { event.status = Object.assign({}, data.status); } if (data.file) { event.output = data.file; } // only source is available, else add item if possible if (data.source) { event.file = data.source; } else if (data.item || data.id) { var item = data.item ? data.item : store.query('GET_ITEM', data.id); event.file = item ? createItemAPI(item) : null; } // map all items in a possible items array if (data.items) { event.items = data.items.map(createItemAPI); } // if this is a progress event add the progress amount if (/progress/.test(name)) { event.progress = data.progress; } // copy relevant props if (data.hasOwnProperty('origin') && data.hasOwnProperty('target')) { event.origin = data.origin; event.target = data.target; } return event; }; }; var eventRoutes = { DID_DESTROY: createEvent('destroy'), DID_INIT: createEvent('init'), DID_THROW_MAX_FILES: createEvent('warning'), DID_INIT_ITEM: createEvent('initfile'), DID_START_ITEM_LOAD: createEvent('addfilestart'), DID_UPDATE_ITEM_LOAD_PROGRESS: createEvent('addfileprogress'), DID_LOAD_ITEM: createEvent('addfile'), DID_THROW_ITEM_INVALID: [createEvent('error'), createEvent('addfile')], DID_THROW_ITEM_LOAD_ERROR: [createEvent('error'), createEvent('addfile')], DID_THROW_ITEM_REMOVE_ERROR: [createEvent('error'), createEvent('removefile')], DID_PREPARE_OUTPUT: createEvent('preparefile'), DID_START_ITEM_PROCESSING: createEvent('processfilestart'), DID_UPDATE_ITEM_PROCESS_PROGRESS: createEvent('processfileprogress'), DID_ABORT_ITEM_PROCESSING: createEvent('processfileabort'), DID_COMPLETE_ITEM_PROCESSING: createEvent('processfile'), DID_COMPLETE_ITEM_PROCESSING_ALL: createEvent('processfiles'), DID_REVERT_ITEM_PROCESSING: createEvent('processfilerevert'), DID_THROW_ITEM_PROCESSING_ERROR: [createEvent('error'), createEvent('processfile')], DID_REMOVE_ITEM: createEvent('removefile'), DID_UPDATE_ITEMS: createEvent('updatefiles'), DID_ACTIVATE_ITEM: createEvent('activatefile'), DID_REORDER_ITEMS: createEvent('reorderfiles'), }; var exposeEvent = function exposeEvent(event) { // create event object to be dispatched var detail = Object.assign({ pond: exports }, event); delete detail.type; view.element.dispatchEvent( new CustomEvent('FilePond:' + event.type, { // event info detail: detail, // event behaviour bubbles: true, cancelable: true, composed: true, // triggers listeners outside of shadow root }) ); // event object to params used for `on()` event handlers and callbacks `oninit()` var params = []; // if is possible error event, make it the first param if (event.hasOwnProperty('error')) { params.push(event.error); } // file is always section if (event.hasOwnProperty('file')) { params.push(event.file); } // append other props var filtered = ['type', 'error', 'file']; Object.keys(event) .filter(function(key) { return !filtered.includes(key); }) .forEach(function(key) { return params.push(event[key]); }); // on(type, () => { }) exports.fire.apply(exports, [event.type].concat(params)); // oninit = () => {} var handler = store.query('GET_ON' + event.type.toUpperCase()); if (handler) { handler.apply(void 0, params); } }; var routeActionsToEvents = function routeActionsToEvents(actions) { if (!actions.length) return; actions .filter(function(action) { return eventRoutes[action.type]; }) .forEach(function(action) { var routes = eventRoutes[action.type]; (Array.isArray(routes) ? routes : [routes]).forEach(function(route) { // this isn't fantastic, but because of the stacking of settimeouts plugins can handle the did_load before the did_init if (action.type === 'DID_INIT_ITEM') { exposeEvent(route(action.data)); } else { setTimeout(function() { exposeEvent(route(action.data)); }, 0); } }); }); }; // // PUBLIC API ------------------------------------------------------------------------------------- // var setOptions = function setOptions(options) { return store.dispatch('SET_OPTIONS', { options: options }); }; var getFile = function getFile(query) { return store.query('GET_ACTIVE_ITEM', query); }; var prepareFile = function prepareFile(query) { return new Promise(function(resolve, reject) { store.dispatch('REQUEST_ITEM_PREPARE', { query: query, success: function success(item) { resolve(item); }, failure: function failure(error) { reject(error); }, }); }); }; var addFile = function addFile(source) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return new Promise(function(resolve, reject) { addFiles([{ source: source, options: options }], { index: options.index }) .then(function(items) { return resolve(items && items[0]); }) .catch(reject); }); }; var isFilePondFile = function isFilePondFile(obj) { return obj.file && obj.id; }; var removeFile = function removeFile(query, options) { // if only passed options if (typeof query === 'object' && !isFilePondFile(query) && !options) { options = query; query = undefined; } // request item removal store.dispatch('REMOVE_ITEM', Object.assign({}, options, { query: query })); // see if item has been removed return store.query('GET_ACTIVE_ITEM', query) === null; }; var addFiles = function addFiles() { for ( var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++ ) { args[_key] = arguments[_key]; } return new Promise(function(resolve, reject) { var sources = []; var options = {}; // user passed a sources array if (isArray(args[0])) { sources.push.apply(sources, args[0]); Object.assign(options, args[1] || {}); } else { // user passed sources as arguments, last one might be options object var lastArgument = args[args.length - 1]; if (typeof lastArgument === 'object' && !(lastArgument instanceof Blob)) { Object.assign(options, args.pop()); } // add rest to sources sources.push.apply(sources, args); } store.dispatch('ADD_ITEMS', { items: sources, index: options.index, interactionMethod: InteractionMethod.API, success: resolve, failure: reject, }); }); }; var getFiles = function getFiles() { return store.query('GET_ACTIVE_ITEMS'); }; var processFile = function processFile(query) { return new Promise(function(resolve, reject) { store.dispatch('REQUEST_ITEM_PROCESSING', { query: query, success: function success(item) { resolve(item); }, failure: function failure(error) { reject(error); }, }); }); }; var prepareFiles = function prepareFiles() { for ( var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++ ) { args[_key2] = arguments[_key2]; } var queries = Array.isArray(args[0]) ? args[0] : args; var items = queries.length ? queries : getFiles(); return Promise.all(items.map(prepareFile)); }; var processFiles = function processFiles() { for ( var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++ ) { args[_key3] = arguments[_key3]; } var queries = Array.isArray(args[0]) ? args[0] : args; if (!queries.length) { var files = getFiles().filter(function(item) { return ( !(item.status === ItemStatus.IDLE && item.origin === FileOrigin.LOCAL) && item.status !== ItemStatus.PROCESSING && item.status !== ItemStatus.PROCESSING_COMPLETE && item.status !== ItemStatus.PROCESSING_REVERT_ERROR ); }); return Promise.all(files.map(processFile)); } return Promise.all(queries.map(processFile)); }; var removeFiles = function removeFiles() { for ( var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++ ) { args[_key4] = arguments[_key4]; } var queries = Array.isArray(args[0]) ? args[0] : args; var options; if (typeof queries[queries.length - 1] === 'object') { options = queries.pop(); } else if (Array.isArray(args[0])) { options = args[1]; } var files = getFiles(); if (!queries.length) return Promise.all( files.map(function(file) { return removeFile(file, options); }) ); // when removing by index the indexes shift after each file removal so we need to convert indexes to ids var mappedQueries = queries .map(function(query) { return isNumber(query) ? (files[query] ? files[query].id : null) : query; }) .filter(function(query) { return query; }); return mappedQueries.map(function(q) { return removeFile(q, options); }); }; var exports = Object.assign( {}, on(), {}, readWriteApi, {}, createOptionAPI(store, defaultOptions), { /** * Override options defined in options object * @param options */ setOptions: setOptions, /** * Load the given file * @param source - the source of the file (either a File, base64 data uri or url) * @param options - object, { index: 0 } */ addFile: addFile, /** * Load the given files * @param sources - the sources of the files to load * @param options - object, { index: 0 } */ addFiles: addFiles, /** * Returns the file objects matching the given query * @param query { string, number, null } */ getFile: getFile, /** * Upload file with given name * @param query { string, number, null } */ processFile: processFile, /** * Request prepare output for file with given name * @param query { string, number, null } */ prepareFile: prepareFile, /** * Removes a file by its name * @param query { string, number, null } */ removeFile: removeFile, /** * Moves a file to a new location in the files list */ moveFile: function moveFile(query, index) { return store.dispatch('MOVE_ITEM', { query: query, index: index }); }, /** * Returns all files (wrapped in public api) */ getFiles: getFiles, /** * Starts uploading all files */ processFiles: processFiles, /** * Clears all files from the files list */ removeFiles: removeFiles, /** * Starts preparing output of all files */ prepareFiles: prepareFiles, /** * Sort list of files */ sort: function sort(compare) { return store.dispatch('SORT', { compare: compare }); }, /** * Browse the file system for a file */ browse: function browse() { // needs to be trigger directly as user action needs to be traceable (is not traceable in requestAnimationFrame) var input = view.element.querySelector('input[type=file]'); if (input) { input.click(); } }, /** * Destroys the app */ destroy: function destroy() { // request destruction exports.fire('destroy', view.element); // stop active processes (file uploads, fetches, stuff like that) // loop over items and depending on states call abort for ongoing processes store.dispatch('ABORT_ALL'); // destroy view view._destroy(); // stop listening to resize window.removeEventListener('resize', resizeHandler); // stop listening to the visiblitychange event document.removeEventListener('visibilitychange', visibilityHandler); // dispatch destroy store.dispatch('DID_DESTROY'); }, /** * Inserts the plugin before the target element */ insertBefore: function insertBefore$1(element) { return insertBefore(view.element, element); }, /** * Inserts the plugin after the target element */ insertAfter: function insertAfter$1(element) { return insertAfter(view.element, element); }, /** * Appends the plugin to the target element */ appendTo: function appendTo(element) { return element.appendChild(view.element); }, /** * Replaces an element with the app */ replaceElement: function replaceElement(element) { // insert the app before the element insertBefore(view.element, element); // remove the original element element.parentNode.removeChild(element); // remember original element originalElement = element; }, /** * Restores the original element */ restoreElement: function restoreElement() { if (!originalElement) { return; // no element to restore } // restore original element insertAfter(originalElement, view.element); // remove our element view.element.parentNode.removeChild(view.element); // remove reference originalElement = null; }, /** * Returns true if the app root is attached to given element * @param element */ isAttachedTo: function isAttachedTo(element) { return view.element === element || originalElement === element; }, /** * Returns the root element */ element: { get: function get() { return view.element; }, }, /** * Returns the current pond status */ status: { get: function get() { return store.query('GET_STATUS'); }, }, } ); // Done! store.dispatch('DID_INIT'); // create actual api object return createObject(exports); }; var createAppObject = function createAppObject() { var customOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; // default options var defaultOptions = {}; forin(getOptions(), function(key, value) { defaultOptions[key] = value[0]; }); // set app options var app = createApp( Object.assign( {}, defaultOptions, {}, customOptions ) ); // return the plugin instance return app; }; var lowerCaseFirstLetter = function lowerCaseFirstLetter(string) { return string.charAt(0).toLowerCase() + string.slice(1); }; var attributeNameToPropertyName = function attributeNameToPropertyName(attributeName) { return toCamels(attributeName.replace(/^data-/, '')); }; var mapObject = function mapObject(object, propertyMap) { // remove unwanted forin(propertyMap, function(selector, mapping) { forin(object, function(property, value) { // create regexp shortcut var selectorRegExp = new RegExp(selector); // tests if var matches = selectorRegExp.test(property); // no match, skip if (!matches) { return; } // if there's a mapping, the original property is always removed delete object[property]; // should only remove, we done! if (mapping === false) { return; } // move value to new property if (isString(mapping)) { object[mapping] = value; return; } // move to group var group = mapping.group; if (isObject(mapping) && !object[group]) { object[group] = {}; } object[group][lowerCaseFirstLetter(property.replace(selectorRegExp, ''))] = value; }); // do submapping if (mapping.mapping) { mapObject(object[mapping.group], mapping.mapping); } }); }; var getAttributesAsObject = function getAttributesAsObject(node) { var attributeMapping = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; // turn attributes into object var attributes = []; forin(node.attributes, function(index) { attributes.push(node.attributes[index]); }); var output = attributes .filter(function(attribute) { return attribute.name; }) .reduce(function(obj, attribute) { var value = attr(node, attribute.name); obj[attributeNameToPropertyName(attribute.name)] = value === attribute.name ? true : value; return obj; }, {}); // do mapping of object properties mapObject(output, attributeMapping); return output; }; var createAppAtElement = function createAppAtElement(element) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; // how attributes of the input element are mapped to the options for the plugin var attributeMapping = { // translate to other name '^class$': 'className', '^multiple$': 'allowMultiple', '^capture$': 'captureMethod', '^webkitdirectory$': 'allowDirectoriesOnly', // group under single property '^server': { group: 'server', mapping: { '^process': { group: 'process', }, '^revert': { group: 'revert', }, '^fetch': { group: 'fetch', }, '^restore': { group: 'restore', }, '^load': { group: 'load', }, }, }, // don't include in object '^type$': false, '^files$': false, }; // add additional option translators applyFilters('SET_ATTRIBUTE_TO_OPTION_MAP', attributeMapping); // create final options object by setting options object and then overriding options supplied on element var mergedOptions = Object.assign({}, options); var attributeOptions = getAttributesAsObject( element.nodeName === 'FIELDSET' ? element.querySelector('input[type=file]') : element, attributeMapping ); // merge with options object Object.keys(attributeOptions).forEach(function(key) { if (isObject(attributeOptions[key])) { if (!isObject(mergedOptions[key])) { mergedOptions[key] = {}; } Object.assign(mergedOptions[key], attributeOptions[key]); } else { mergedOptions[key] = attributeOptions[key]; } }); // if parent is a fieldset, get files from parent by selecting all input fields that are not file upload fields // these will then be automatically set to the initial files mergedOptions.files = (options.files || []).concat( Array.from(element.querySelectorAll('input:not([type=file])')).map(function(input) { return { source: input.value, options: { type: input.dataset.type, }, }; }) ); // build plugin var app = createAppObject(mergedOptions); // add already selected files if (element.files) { Array.from(element.files).forEach(function(file) { app.addFile(file); }); } // replace the target element app.replaceElement(element); // expose return app; }; // if an element is passed, we create the instance at that element, if not, we just create an up object var createApp$1 = function createApp() { return isNode(arguments.length <= 0 ? undefined : arguments[0]) ? createAppAtElement.apply(void 0, arguments) : createAppObject.apply(void 0, arguments); }; var PRIVATE_METHODS = ['fire', '_read', '_write']; var createAppAPI = function createAppAPI(app) { var api = {}; copyObjectPropertiesToObject(app, api, PRIVATE_METHODS); return api; }; /** * Replaces placeholders in given string with replacements * @param string - "Foo {bar}"" * @param replacements - { "bar": 10 } */ var replaceInString = function replaceInString(string, replacements) { return string.replace(/(?:{([a-zA-Z]+)})/g, function(match, group) { return replacements[group]; }); }; var createWorker = function createWorker(fn) { var workerBlob = new Blob(['(', fn.toString(), ')()'], { type: 'application/javascript', }); var workerURL = URL.createObjectURL(workerBlob); var worker = new Worker(workerURL); return { transfer: function transfer(message, cb) {}, post: function post(message, cb, transferList) { var id = getUniqueId(); worker.onmessage = function(e) { if (e.data.id === id) { cb(e.data.message); } }; worker.postMessage( { id: id, message: message, }, transferList ); }, terminate: function terminate() { worker.terminate(); URL.revokeObjectURL(workerURL); }, }; }; var loadImage = function loadImage(url) { return new Promise(function(resolve, reject) { var img = new Image(); img.onload = function() { resolve(img); }; img.onerror = function(e) { reject(e); }; img.src = url; }); }; var renameFile = function renameFile(file, name) { var renamedFile = file.slice(0, file.size, file.type); renamedFile.lastModifiedDate = file.lastModifiedDate; renamedFile.name = name; return renamedFile; }; var copyFile = function copyFile(file) { return renameFile(file, file.name); }; // already registered plugins (can't register twice) var registeredPlugins = []; // pass utils to plugin var createAppPlugin = function createAppPlugin(plugin) { // already registered if (registeredPlugins.includes(plugin)) { return; } // remember this plugin registeredPlugins.push(plugin); // setup! var pluginOutline = plugin({ addFilter: addFilter, utils: { Type: Type, forin: forin, isString: isString, isFile: isFile, toNaturalFileSize: toNaturalFileSize, replaceInString: replaceInString, getExtensionFromFilename: getExtensionFromFilename, getFilenameWithoutExtension: getFilenameWithoutExtension, guesstimateMimeType: guesstimateMimeType, getFileFromBlob: getFileFromBlob, getFilenameFromURL: getFilenameFromURL, createRoute: createRoute, createWorker: createWorker, createView: createView, createItemAPI: createItemAPI, loadImage: loadImage, copyFile: copyFile, renameFile: renameFile, createBlob: createBlob, applyFilterChain: applyFilterChain, text: text, getNumericAspectRatioFromString: getNumericAspectRatioFromString, }, views: { fileActionButton: fileActionButton, }, }); // add plugin options to default options extendDefaultOptions(pluginOutline.options); }; // feature detection used by supported() method var isOperaMini = function isOperaMini() { return Object.prototype.toString.call(window.operamini) === '[object OperaMini]'; }; var hasPromises = function hasPromises() { return 'Promise' in window; }; var hasBlobSlice = function hasBlobSlice() { return 'slice' in Blob.prototype; }; var hasCreateObjectURL = function hasCreateObjectURL() { return 'URL' in window && 'createObjectURL' in window.URL; }; var hasVisibility = function hasVisibility() { return 'visibilityState' in document; }; var hasTiming = function hasTiming() { return 'performance' in window; }; // iOS 8.x var hasCSSSupports = function hasCSSSupports() { return 'supports' in (window.CSS || {}); }; // use to detect Safari 9+ var isIE11 = function isIE11() { return /MSIE|Trident/.test(window.navigator.userAgent); }; var supported = (function() { // Runs immediately and then remembers result for subsequent calls var isSupported = // Has to be a browser isBrowser() && // Can't run on Opera Mini due to lack of everything !isOperaMini() && // Require these APIs to feature detect a modern browser hasVisibility() && hasPromises() && hasBlobSlice() && hasCreateObjectURL() && hasTiming() && // doesn't need CSSSupports but is a good way to detect Safari 9+ (we do want to support IE11 though) (hasCSSSupports() || isIE11()); return function() { return isSupported; }; })(); /** * Plugin internal state (over all instances) */ var state = { // active app instances, used to redraw the apps and to find the later apps: [], }; // plugin name var name = 'filepond'; /** * Public Plugin methods */ var fn = function fn() {}; exports.Status = {}; exports.FileStatus = {}; exports.FileOrigin = {}; exports.OptionTypes = {}; exports.create = fn; exports.destroy = fn; exports.parse = fn; exports.find = fn; exports.registerPlugin = fn; exports.getOptions = fn; exports.setOptions = fn; // if not supported, no API if (supported()) { // start painter and fire load event createPainter( function() { state.apps.forEach(function(app) { return app._read(); }); }, function(ts) { state.apps.forEach(function(app) { return app._write(ts); }); } ); // fire loaded event so we know when FilePond is available var dispatch = function dispatch() { // let others know we have area ready document.dispatchEvent( new CustomEvent('FilePond:loaded', { detail: { supported: supported, create: exports.create, destroy: exports.destroy, parse: exports.parse, find: exports.find, registerPlugin: exports.registerPlugin, setOptions: exports.setOptions, }, }) ); // clean up event document.removeEventListener('DOMContentLoaded', dispatch); }; if (document.readyState !== 'loading') { // move to back of execution queue, FilePond should have been exported by then setTimeout(function() { return dispatch(); }, 0); } else { document.addEventListener('DOMContentLoaded', dispatch); } // updates the OptionTypes object based on the current options var updateOptionTypes = function updateOptionTypes() { return forin(getOptions(), function(key, value) { exports.OptionTypes[key] = value[1]; }); }; exports.Status = Object.assign({}, Status); exports.FileOrigin = Object.assign({}, FileOrigin); exports.FileStatus = Object.assign({}, ItemStatus); exports.OptionTypes = {}; updateOptionTypes(); // create method, creates apps and adds them to the app array exports.create = function create() { var app = createApp$1.apply(void 0, arguments); app.on('destroy', exports.destroy); state.apps.push(app); return createAppAPI(app); }; // destroys apps and removes them from the app array exports.destroy = function destroy(hook) { // returns true if the app was destroyed successfully var indexToRemove = state.apps.findIndex(function(app) { return app.isAttachedTo(hook); }); if (indexToRemove >= 0) { // remove from apps var app = state.apps.splice(indexToRemove, 1)[0]; // restore original dom element app.restoreElement(); return true; } return false; }; // parses the given context for plugins (does not include the context element itself) exports.parse = function parse(context) { // get all possible hooks var matchedHooks = Array.from(context.querySelectorAll('.' + name)); // filter out already active hooks var newHooks = matchedHooks.filter(function(newHook) { return !state.apps.find(function(app) { return app.isAttachedTo(newHook); }); }); // create new instance for each hook return newHooks.map(function(hook) { return exports.create(hook); }); }; // returns an app based on the given element hook exports.find = function find(hook) { var app = state.apps.find(function(app) { return app.isAttachedTo(hook); }); if (!app) { return null; } return createAppAPI(app); }; // adds a plugin extension exports.registerPlugin = function registerPlugin() { for ( var _len = arguments.length, plugins = new Array(_len), _key = 0; _key < _len; _key++ ) { plugins[_key] = arguments[_key]; } // register plugins plugins.forEach(createAppPlugin); // update OptionTypes, each plugin might have extended the default options updateOptionTypes(); }; exports.getOptions = function getOptions$1() { var opts = {}; forin(getOptions(), function(key, value) { opts[key] = value[0]; }); return opts; }; exports.setOptions = function setOptions$1(opts) { if (isObject(opts)) { // update existing plugins state.apps.forEach(function(app) { app.setOptions(opts); }); // override defaults setOptions(opts); } // return new options return exports.getOptions(); }; } exports.supported = supported; Object.defineProperty(exports, '__esModule', { value: true }); }); ================================================ FILE: index.html ================================================ FilePond Demo ================================================ FILE: jest.config.js ================================================ module.exports = { bail: true, verbose: true, setupFiles: ['/jest.stubs.js'], roots: ['/src/js'] }; ================================================ FILE: jest.stubs.js ================================================ const uuid = require('uuid/v4'); // needed because jest doesn't have CSS on window if (!window.CSS) window.CSS = {}; window.CSS.supports = () => true; window.URL.createObjectURL = blob => { return `blob:${serializeURL(location.origin)}/${uuid()}`; }; window.URL.revokeObjectURL = url => {}; ================================================ FILE: locale/am-et.js ================================================ export default { labelIdle: 'ፋይሎች ስበው እዚህ ጋር ይልቀቁት ወይም ፋይሉን ይምረጡ ', labelInvalidField: 'መስኩ ልክ ያልሆኑ ፋይሎችን ይዟል', labelFileWaitingForSize: 'የፋይሉን መጠን በመጠባበቅ ላይ', labelFileSizeNotAvailable: 'የፋይሉን መጠን ሊገኝ አልቻለም', labelFileLoading: 'በማንበብ ላይ', labelFileLoadError: 'በማንበብ ላይ ችግር ተፈጥሯል', labelFileProcessing: 'ፋይሉን በመጫን ላይ', labelFileProcessingComplete: 'ፋይሉን መጫን ተጠናቅቋል', labelFileProcessingAborted: 'ፋይሉን መጫን ተቋርጧል', labelFileProcessingError: 'ፋይሉን በመጫን ላይ ችግር ተፈጥሯል', labelFileProcessingRevertError: 'ፈይሉን በመቀልበስ ላይ ችግር ተፈጥሯል', labelFileRemoveError: 'በማጥፋት ላይ ችግር ተፈጥሯል', labelTapToCancel: 'ለማቋረጥ ነካ ያድርጉ', labelTapToRetry: 'ደግሞ ለመሞከር ነካ ያድርጉ', labelTapToUndo: 'ወደነበረበት ለመመለስ ነካ ያድርጉ', labelButtonRemoveItem: 'ላጥፋ', labelButtonAbortItemLoad: 'ላቋርጥ', labelButtonRetryItemLoad: 'ደግሜ ልሞክር', labelButtonAbortItemProcessing: 'ይቅር', labelButtonUndoItemProcessing: 'ወደነበረበት ልመልስ', labelButtonRetryItemProcessing: 'ደግሜ ልሞክር', labelButtonProcessItem: 'ልጫን', labelMaxFileSizeExceeded: 'ፋይሉ ተልቋል', labelMaxFileSize: 'የፋይል መጠን ከ {filesize} መብለጥ አይፈቀድም', labelMaxTotalFileSizeExceeded: 'የሚፈቀደውን ጠቅላላ የፋይል መጠን አልፈዋል', labelMaxTotalFileSize: 'ጠቅላላ የፋይል መጠን ከ {filesize} መብለጥ አይፈቀድም', labelFileTypeNotAllowed: 'የተሳሳተ የፋይል አይነት ነው', fileValidateTypeLabelExpectedTypes: 'የፋይል አይነቱ መሆን የሚገባው {allButLastType} እና {lastType} ነው', imageValidateSizeLabelFormatError: 'የምስል አይነቱ ለመጫን አይሆንም', imageValidateSizeLabelImageSizeTooSmall: 'ምስሉ በጣም አንሷል', imageValidateSizeLabelImageSizeTooBig: 'ምስሉ በጣም ተልቋል', imageValidateSizeLabelExpectedMinSize: 'ዝቅተኛው የምስል ልኬት {minWidth} × {minHeight} ነው', imageValidateSizeLabelExpectedMaxSize: 'ከፍተኛው የምስል ልኬት {maxWidth} × {maxHeight} ነው', imageValidateSizeLabelImageResolutionTooLow: 'የምስሉ ጥራት በጣም ዝቅተኛ ነው', imageValidateSizeLabelImageResolutionTooHigh: 'የምስሉ ጥራት በጣም ከፍተኛ ነው', imageValidateSizeLabelExpectedMinResolution: 'ዝቅተኛው የምስል ጥራት {minResolution} ነው', imageValidateSizeLabelExpectedMaxResolution: 'ከፍተኛው የምስል ጥራት {maxResolution} ነው' }; ================================================ FILE: locale/ar-ar.js ================================================ export default { labelIdle: 'اسحب و ادرج ملفاتك أو تصفح ', labelInvalidField: 'الحقل يحتوي على ملفات غير صالحة', labelFileWaitingForSize: 'بانتظار الحجم', labelFileSizeNotAvailable: 'الحجم غير متاح', labelFileLoading: 'بالإنتظار', labelFileLoadError: 'حدث خطأ أثناء التحميل', labelFileProcessing: 'يتم الرفع', labelFileProcessingComplete: 'تم الرفع', labelFileProcessingAborted: 'تم إلغاء الرفع', labelFileProcessingError: 'حدث خطأ أثناء الرفع', labelFileProcessingRevertError: 'حدث خطأ أثناء التراجع', labelFileRemoveError: 'حدث خطأ أثناء الحذف', labelTapToCancel: 'انقر للإلغاء', labelTapToRetry: 'انقر لإعادة المحاولة', labelTapToUndo: 'انقر للتراجع', labelButtonRemoveItem: 'مسح', labelButtonAbortItemLoad: 'إلغاء', labelButtonRetryItemLoad: 'إعادة', labelButtonAbortItemProcessing: 'إلغاء', labelButtonUndoItemProcessing: 'تراجع', labelButtonRetryItemProcessing: 'إعادة', labelButtonProcessItem: 'رفع', labelMaxFileSizeExceeded: 'الملف كبير جدا', labelMaxFileSize: 'حجم الملف الأقصى: {filesize}', labelMaxTotalFileSizeExceeded: 'تم تجاوز الحد الأقصى للحجم الإجمالي', labelMaxTotalFileSize: 'الحد الأقصى لحجم الملف: {filesize}', labelFileTypeNotAllowed: 'ملف من نوع غير صالح', fileValidateTypeLabelExpectedTypes: 'تتوقع {allButLastType} من {lastType}', imageValidateSizeLabelFormatError: 'نوع الصورة غير مدعوم', imageValidateSizeLabelImageSizeTooSmall: 'الصورة صغير جدا', imageValidateSizeLabelImageSizeTooBig: 'الصورة كبيرة جدا', imageValidateSizeLabelExpectedMinSize: 'الحد الأدنى للأبعاد هو: {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'الحد الأقصى للأبعاد هو: {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'الدقة ضعيفة جدا', imageValidateSizeLabelImageResolutionTooHigh: 'الدقة مرتفعة جدا', imageValidateSizeLabelExpectedMinResolution: 'أقل دقة: {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'أقصى دقة: {maxResolution}' }; ================================================ FILE: locale/az-az.js ================================================ export default { labelIdle: 'Faylınızı Sürüşdürün & Buraxın ya da Seçin ', labelInvalidField: 'Sahədə etibarsız fayllar var', labelFileWaitingForSize: 'Ölçü hesablanır', labelFileSizeNotAvailable: 'Ölçü mövcud deyil', labelFileLoading: 'Yüklənir', labelFileLoadError: 'Yükləmə əsnasında xəta baş verdi', labelFileProcessing: 'Yüklənir', labelFileProcessingComplete: 'Yükləmə tamamlandı', labelFileProcessingAborted: 'Yükləmə ləğv edildi', labelFileProcessingError: 'Yükəyərkən xəta baş verdi', labelFileProcessingRevertError: 'Geri çəkərkən xəta baş verdi', labelFileRemoveError: 'Çıxararkən xəta baş verdi', labelTapToCancel: 'İmtina etmək üçün klikləyin', labelTapToRetry: 'Təkrar yoxlamaq üçün klikləyin', labelTapToUndo: 'Geri almaq üçün klikləyin', labelButtonRemoveItem: 'Çıxar', labelButtonAbortItemLoad: 'İmtina Et', labelButtonRetryItemLoad: 'Təkrar yoxla', labelButtonAbortItemProcessing: 'İmtina et', labelButtonUndoItemProcessing: 'Geri Al', labelButtonRetryItemProcessing: 'Təkrar yoxla', labelButtonProcessItem: 'Yüklə', labelMaxFileSizeExceeded: 'Fayl çox böyükdür', labelMaxFileSize: 'Ən böyük fayl ölçüsü: {filesize}', labelMaxTotalFileSizeExceeded: 'Maksimum ölçü keçildi', labelMaxTotalFileSize: 'Maksimum fayl ölçüsü :{filesize}', labelFileTypeNotAllowed: 'Etibarsız fayl tipi', fileValidateTypeLabelExpectedTypes: 'Bu {allButLastType} ya da bu fayl olması lazımdır: {lastType}', imageValidateSizeLabelFormatError: 'Şəkil tipi dəstəklənmir', imageValidateSizeLabelImageSizeTooSmall: 'Şəkil çox kiçik', imageValidateSizeLabelImageSizeTooBig: 'Şəkil çox böyük', imageValidateSizeLabelExpectedMinSize: 'Minimum ölçü {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maksimum ölçü {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Görüntü imkanı çox aşağı', imageValidateSizeLabelImageResolutionTooHigh: 'Görüntü imkanı çox yüksək', imageValidateSizeLabelExpectedMinResolution: 'Minimum görüntü imkanı {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maximum görüntü imkanı {maxResolution}' }; ================================================ FILE: locale/ca-ca.js ================================================ export default { labelIdle: `Arrossega i deixa anar els teus fitxers o Navega `, labelInvalidField: `El camp conté fitxers invàlids`, labelFileWaitingForSize: `Esperant mida`, labelFileSizeNotAvailable: `Mida no disponible`, labelFileLoading: `Carregant`, labelFileLoadError: `Error durant la càrrega`, labelFileProcessing: `Pujant`, labelFileProcessingComplete: `Pujada completada`, labelFileProcessingAborted: `Pujada cancel·lada`, labelFileProcessingError: `Error durant la pujada`, labelFileProcessingRevertError: `Error durant la reversió`, labelFileRemoveError: `Error durant l'eliminació`, labelTapToCancel: `toca per cancel·lar`, labelTapToRetry: `toca per reintentar`, labelTapToUndo: `toca per desfer`, labelButtonRemoveItem: `Eliminar`, labelButtonAbortItemLoad: `Cancel·lar`, labelButtonRetryItemLoad: `Reintentar`, labelButtonAbortItemProcessing: `Cancel·lar`, labelButtonUndoItemProcessing: `Desfer`, labelButtonRetryItemProcessing: `Reintentar`, labelButtonProcessItem: `Pujar`, labelMaxFileSizeExceeded: `El fitxer és massa gran`, labelMaxFileSize: `La mida màxima del fitxer és {filesize}`, labelMaxTotalFileSizeExceeded: `Mida màxima total excedida`, labelMaxTotalFileSize: `La mida màxima total del fitxer és {filesize}`, labelFileTypeNotAllowed: `Fitxer de tipus invàlid`, fileValidateTypeLabelExpectedTypes: `Espera {allButLastType} o {lastType}`, imageValidateSizeLabelFormatError: `Tipus d'imatge no suportada`, imageValidateSizeLabelImageSizeTooSmall: `La imatge és massa petita`, imageValidateSizeLabelImageSizeTooBig: `La imatge és massa gran`, imageValidateSizeLabelExpectedMinSize: `La mida mínima és {minWidth} x {minHeight}`, imageValidateSizeLabelExpectedMaxSize: `La mida màxima és {maxWidth} x {maxHeight}`, imageValidateSizeLabelImageResolutionTooLow: `La resolució és massa baixa`, imageValidateSizeLabelImageResolutionTooHigh: `La resolució és massa alta`, imageValidateSizeLabelExpectedMinResolution: `La resolució mínima és {minResolution}`, imageValidateSizeLabelExpectedMaxResolution: `La resolució màxima és {maxResolution}`, } ================================================ FILE: locale/cs-cz.js ================================================ export default { labelIdle: 'Přetáhněte soubor sem (drag&drop) nebo Vyhledat ', labelInvalidField: 'Pole obsahuje chybné soubory', labelFileWaitingForSize: 'Zjišťuje se velikost', labelFileSizeNotAvailable: 'Velikost není známá', labelFileLoading: 'Přenáší se', labelFileLoadError: 'Chyba při přenosu', labelFileProcessing: 'Probíhá upload', labelFileProcessingComplete: 'Upload dokončen', labelFileProcessingAborted: 'Upload stornován', labelFileProcessingError: 'Chyba při uploadu', labelFileProcessingRevertError: 'Chyba při obnově', labelFileRemoveError: 'Chyba při odstranění', labelTapToCancel: 'klepněte pro storno', labelTapToRetry: 'klepněte pro opakování', labelTapToUndo: 'klepněte pro vrácení', labelButtonRemoveItem: 'Odstranit', labelButtonAbortItemLoad: 'Storno', labelButtonRetryItemLoad: 'Opakovat', labelButtonAbortItemProcessing: 'Zpět', labelButtonUndoItemProcessing: 'Vrátit', labelButtonRetryItemProcessing: 'Opakovat', labelButtonProcessItem: 'Upload', labelMaxFileSizeExceeded: 'Soubor je příliš velký', labelMaxFileSize: 'Největší velikost souboru je {filesize}', labelMaxTotalFileSizeExceeded: 'Překročena maximální celková velikost souboru', labelMaxTotalFileSize: 'Maximální celková velikost souboru je {filesize}', labelFileTypeNotAllowed: 'Soubor je nesprávného typu', fileValidateTypeLabelExpectedTypes: 'Očekává se {allButLastType} nebo {lastType}', imageValidateSizeLabelFormatError: 'Obrázek tohoto typu není podporován', imageValidateSizeLabelImageSizeTooSmall: 'Obrázek je příliš malý', imageValidateSizeLabelImageSizeTooBig: 'Obrázek je příliš velký', imageValidateSizeLabelExpectedMinSize: 'Minimální rozměr je {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maximální rozměr je {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Rozlišení je příliš malé', imageValidateSizeLabelImageResolutionTooHigh: 'Rozlišení je příliš velké', imageValidateSizeLabelExpectedMinResolution: 'Minimální rozlišení je {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maximální rozlišení je {maxResolution}' }; ================================================ FILE: locale/cy-cy.js ================================================ export default { labelIdle: 'Llusgwch a Gollyngwch eich ffeiliau neu ddefnyddiwch y Porwr ', labelInvalidField: 'Mae’r maes yn cynnwys ffeiliau annilys', labelFileWaitingForSize: 'Disgwyl am y maint', labelFileSizeNotAvailable: 'Dim maint ar gael', labelFileLoading: 'Llwytho', labelFileLoadError: 'Camgymeriad wrth lwytho', labelFileProcessing: 'Lanlwytho', labelFileProcessingComplete: 'Lanlwythiad yn gyflawn', labelFileProcessingAborted: 'Lanlwythiad wedi ei ganslo', labelFileProcessingError: 'Camgymeriad wrth lanlwytho', labelFileProcessingRevertError: 'Camgymeriad wrth ddychwelyd', labelFileRemoveError: 'Camgymeriad wrth ddileu', labelTapToCancel: 'tapiwch i ganslo', labelTapToRetry: 'tapiwch i roi cynnig arall arni', labelTapToUndo: 'tapiwch i ddadwneud', labelButtonRemoveItem: 'Dileu', labelButtonAbortItemLoad: 'Atal', labelButtonRetryItemLoad: 'Ceisio eto', labelButtonAbortItemProcessing: 'Canslo', labelButtonUndoItemProcessing: 'Dadwneud', labelButtonRetryItemProcessing: 'Ceisio eto', labelButtonProcessItem: 'Lanlwytho', labelMaxFileSizeExceeded: 'Mae\'r ffeil yn rhy fawr', labelMaxFileSize: 'Uchafswm maint y ffeil yw {filesize}', labelMaxTotalFileSizeExceeded: 'Mwy na’r cyfanswm uchaf o ran maint', labelMaxTotalFileSize: 'Uchafswm maint y ffeil yw {filesize}', labelFileTypeNotAllowed: 'Math o ffeil annilys', fileValidateTypeLabelExpectedTypes: 'Yn disgwyl {allButLastType} neu {lastType}', imageValidateSizeLabelFormatError: 'Ni chefnogir y math o ddelwedd', imageValidateSizeLabelImageSizeTooSmall: 'Mae\'r ddelwedd yn rhy fach', imageValidateSizeLabelImageSizeTooBig: 'Mae\'r ddelwedd yn rhy fawr', imageValidateSizeLabelExpectedMinSize: 'Y maint lleiaf yw {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Y maint mwyaf yw {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Mae\'r cydraniad yn rhy isel', imageValidateSizeLabelImageResolutionTooHigh: 'Mae\'r cydraniad yn rhy uchel', imageValidateSizeLabelExpectedMinResolution: 'Y cydraniad lleiaf yw {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Y cydraniad uchaf yw {maxResolution}' }; ================================================ FILE: locale/da-dk.js ================================================ export default { labelIdle: 'Træk & slip filer eller Gennemse ', labelInvalidField: 'Felt indeholder ugyldige filer', labelFileWaitingForSize: 'Venter på størrelse', labelFileSizeNotAvailable: 'Størrelse ikke tilgængelig', labelFileLoading: 'Loader', labelFileLoadError: 'Load fejlede', labelFileProcessing: 'Uploader', labelFileProcessingComplete: 'Upload færdig', labelFileProcessingAborted: 'Upload annulleret', labelFileProcessingError: 'Upload fejlede', labelFileProcessingRevertError: 'Fortryd fejlede', labelFileRemoveError: 'Fjern fejlede', labelTapToCancel: 'tryk for at annullere', labelTapToRetry: 'tryk for at prøve igen', labelTapToUndo: 'tryk for at fortryde', labelButtonRemoveItem: 'Fjern', labelButtonAbortItemLoad: 'Annuller', labelButtonRetryItemLoad: 'Forsøg igen', labelButtonAbortItemProcessing: 'Annuller', labelButtonUndoItemProcessing: 'Fortryd', labelButtonRetryItemProcessing: 'Prøv igen', labelButtonProcessItem: 'Upload', labelMaxFileSizeExceeded: 'Filen er for stor', labelMaxFileSize: 'Maksimal filstørrelse er {filesize}', labelMaxTotalFileSizeExceeded: 'Maksimal totalstørrelse overskredet', labelMaxTotalFileSize: 'Maksimal total filstørrelse er {filesize}', labelFileTypeNotAllowed: 'Ugyldig filtype', fileValidateTypeLabelExpectedTypes: 'Forventer {allButLastType} eller {lastType}', imageValidateSizeLabelFormatError: 'Ugyldigt format', imageValidateSizeLabelImageSizeTooSmall: 'Billedet er for lille', imageValidateSizeLabelImageSizeTooBig: 'Billedet er for stort', imageValidateSizeLabelExpectedMinSize: 'Minimum størrelse er {minBredde} × {minHøjde}', imageValidateSizeLabelExpectedMaxSize: 'Maksimal størrelse er {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'For lav opløsning', imageValidateSizeLabelImageResolutionTooHigh: 'For høj opløsning', imageValidateSizeLabelExpectedMinResolution: 'Minimum opløsning er {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maksimal opløsning er {maxResolution}', }; ================================================ FILE: locale/de-de.js ================================================ export default { labelIdle: 'Dateien ablegen oder auswählen ', labelInvalidField: 'Feld beinhaltet ungültige Dateien', labelFileWaitingForSize: 'Dateigröße berechnen', labelFileSizeNotAvailable: 'Dateigröße nicht verfügbar', labelFileLoading: 'Laden', labelFileLoadError: 'Fehler beim Laden', labelFileProcessing: 'Upload läuft', labelFileProcessingComplete: 'Upload abgeschlossen', labelFileProcessingAborted: 'Upload abgebrochen', labelFileProcessingError: 'Fehler beim Upload', labelFileProcessingRevertError: 'Fehler beim Wiederherstellen', labelFileRemoveError: 'Fehler beim Löschen', labelTapToCancel: 'abbrechen', labelTapToRetry: 'erneut versuchen', labelTapToUndo: 'rückgängig', labelButtonRemoveItem: 'Entfernen', labelButtonAbortItemLoad: 'Verwerfen', labelButtonRetryItemLoad: 'Erneut versuchen', labelButtonAbortItemProcessing: 'Abbrechen', labelButtonUndoItemProcessing: 'Rückgängig', labelButtonRetryItemProcessing: 'Erneut versuchen', labelButtonProcessItem: 'Upload', labelMaxFileSizeExceeded: 'Datei ist zu groß', labelMaxFileSize: 'Maximale Dateigröße: {filesize}', labelMaxTotalFileSizeExceeded: 'Maximale gesamte Dateigröße überschritten', labelMaxTotalFileSize: 'Maximale gesamte Dateigröße: {filesize}', labelFileTypeNotAllowed: 'Dateityp ungültig', fileValidateTypeLabelExpectedTypes: 'Erwartet {allButLastType} oder {lastType}', imageValidateSizeLabelFormatError: 'Bildtyp nicht unterstützt', imageValidateSizeLabelImageSizeTooSmall: 'Bild ist zu klein', imageValidateSizeLabelImageSizeTooBig: 'Bild ist zu groß', imageValidateSizeLabelExpectedMinSize: 'Mindestgröße: {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maximale Größe: {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Auflösung ist zu niedrig', imageValidateSizeLabelImageResolutionTooHigh: 'Auflösung ist zu hoch', imageValidateSizeLabelExpectedMinResolution: 'Mindestauflösung: {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maximale Auflösung: {maxResolution}' }; ================================================ FILE: locale/el-el.js ================================================ export default { labelIdle: 'Σύρετε τα αρχεία σας στο πλαίσιο ή Επιλέξτε ', labelInvalidField: 'Το πεδίο περιέχει μη έγκυρα αρχεία', labelFileWaitingForSize: 'Σε αναμονή για το μέγεθος', labelFileSizeNotAvailable: 'Μέγεθος μη διαθέσιμο', labelFileLoading: 'Φόρτωση σε εξέλιξη', labelFileLoadError: 'Σφάλμα κατά τη φόρτωση', labelFileProcessing: 'Επεξεργασία', labelFileProcessingComplete: 'Η επεξεργασία ολοκληρώθηκε', labelFileProcessingAborted: 'Η επεξεργασία ακυρώθηκε', labelFileProcessingError: 'Σφάλμα κατά την επεξεργασία', labelFileProcessingRevertError: 'Σφάλμα κατά την επαναφορά', labelFileRemoveError: 'Σφάλμα κατά την διαγραφή', labelTapToCancel: 'πατήστε για ακύρωση', labelTapToRetry: 'πατήστε για επανάληψη', labelTapToUndo: 'πατήστε για αναίρεση', labelButtonRemoveItem: 'Αφαίρεση', labelButtonAbortItemLoad: 'Ακύρωση', labelButtonRetryItemLoad: 'Επανάληψη', labelButtonAbortItemProcessing: 'Ακύρωση', labelButtonUndoItemProcessing: 'Αναίρεση', labelButtonRetryItemProcessing: 'Επανάληψη', labelButtonProcessItem: 'Μεταφόρτωση', labelMaxFileSizeExceeded: 'Το αρχείο είναι πολύ μεγάλο', labelMaxFileSize: 'Το μέγιστο μέγεθος αρχείου είναι {filesize}', labelMaxTotalFileSizeExceeded: 'Υπέρβαση του μέγιστου συνολικού μεγέθους', labelMaxTotalFileSize: 'Το μέγιστο συνολικό μέγεθος αρχείων είναι {filesize}', labelFileTypeNotAllowed: 'Μη έγκυρος τύπος αρχείου', fileValidateTypeLabelExpectedTypes: 'Τα αποδεκτά αρχεία είναι {allButLastType} ή {lastType}', imageValidateSizeLabelFormatError: 'Ο τύπος της εικόνας δεν υποστηρίζεται', imageValidateSizeLabelImageSizeTooSmall: 'Η εικόνα είναι πολύ μικρή', imageValidateSizeLabelImageSizeTooBig: 'Η εικόνα είναι πολύ μεγάλη', imageValidateSizeLabelExpectedMinSize: 'Το ελάχιστο αποδεκτό μέγεθος είναι {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Το μέγιστο αποδεκτό μέγεθος είναι {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Η ανάλυση της εικόνας είναι πολύ χαμηλή', imageValidateSizeLabelImageResolutionTooHigh: 'Η ανάλυση της εικόνας είναι πολύ υψηλή', imageValidateSizeLabelExpectedMinResolution: 'Η ελάχιστη αποδεκτή ανάλυση είναι {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Η μέγιστη αποδεκτή ανάλυση είναι {maxResolution}' }; ================================================ FILE: locale/en-en.js ================================================ export default { labelIdle: 'Drag & Drop your files or Browse ', labelInvalidField: 'Field contains invalid files', labelFileWaitingForSize: 'Waiting for size', labelFileSizeNotAvailable: 'Size not available', labelFileLoading: 'Loading', labelFileLoadError: 'Error during load', labelFileProcessing: 'Uploading', labelFileProcessingComplete: 'Upload complete', labelFileProcessingAborted: 'Upload cancelled', labelFileProcessingError: 'Error during upload', labelFileProcessingRevertError: 'Error during revert', labelFileRemoveError: 'Error during remove', labelTapToCancel: 'tap to cancel', labelTapToRetry: 'tap to retry', labelTapToUndo: 'tap to undo', labelButtonRemoveItem: 'Remove', labelButtonAbortItemLoad: 'Abort', labelButtonRetryItemLoad: 'Retry', labelButtonAbortItemProcessing: 'Cancel', labelButtonUndoItemProcessing: 'Undo', labelButtonRetryItemProcessing: 'Retry', labelButtonProcessItem: 'Upload', labelMaxFileSizeExceeded: 'File is too large', labelMaxFileSize: 'Maximum file size is {filesize}', labelMaxTotalFileSizeExceeded: 'Maximum total size exceeded', labelMaxTotalFileSize: 'Maximum total file size is {filesize}', labelFileTypeNotAllowed: 'File of invalid type', fileValidateTypeLabelExpectedTypes: 'Expects {allButLastType} or {lastType}', imageValidateSizeLabelFormatError: 'Image type not supported', imageValidateSizeLabelImageSizeTooSmall: 'Image is too small', imageValidateSizeLabelImageSizeTooBig: 'Image is too big', imageValidateSizeLabelExpectedMinSize: 'Minimum size is {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maximum size is {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Resolution is too low', imageValidateSizeLabelImageResolutionTooHigh: 'Resolution is too high', imageValidateSizeLabelExpectedMinResolution: 'Minimum resolution is {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maximum resolution is {maxResolution}' }; ================================================ FILE: locale/es-es.js ================================================ export default { labelIdle: 'Arrastra y suelta tus archivos o Examina ', labelInvalidField: "El campo contiene archivos inválidos", labelFileWaitingForSize: "Esperando tamaño", labelFileSizeNotAvailable: "Tamaño no disponible", labelFileLoading: "Cargando", labelFileLoadError: "Error durante la carga", labelFileProcessing: "Subiendo", labelFileProcessingComplete: "Subida completa", labelFileProcessingAborted: "Subida cancelada", labelFileProcessingError: "Error durante la subida", labelFileProcessingRevertError: "Error durante la reversión", labelFileRemoveError: "Error durante la eliminación", labelTapToCancel: "toca para cancelar", labelTapToRetry: "tocar para reintentar", labelTapToUndo: "tocar para deshacer", labelButtonRemoveItem: "Eliminar", labelButtonAbortItemLoad: "Cancelar", labelButtonRetryItemLoad: "Reintentar", labelButtonAbortItemProcessing: "Cancelar", labelButtonUndoItemProcessing: "Deshacer", labelButtonRetryItemProcessing: "Reintentar", labelButtonProcessItem: "Subir", labelMaxFileSizeExceeded: "El archivo es demasiado grande", labelMaxFileSize: "El tamaño máximo del archivo es {filesize}", labelMaxTotalFileSizeExceeded: "Tamaño total máximo excedido", labelMaxTotalFileSize: "El tamaño total máximo del archivo es {filesize}", labelFileTypeNotAllowed: "Archivo de tipo inválido", fileValidateTypeLabelExpectedTypes: "Espera {allButLastType} o {lastType}", imageValidateSizeLabelFormatError: "Tipo de imagen no soportada", imageValidateSizeLabelImageSizeTooSmall: "La imagen es demasiado pequeña", imageValidateSizeLabelImageSizeTooBig: "La imagen es demasiado grande", imageValidateSizeLabelExpectedMinSize: "El tamaño mínimo es {minWidth} x {minHeight}", imageValidateSizeLabelExpectedMaxSize: "El tamaño máximo es {maxWidth} x {maxHeight}", imageValidateSizeLabelImageResolutionTooLow: "La resolución es demasiado baja", imageValidateSizeLabelImageResolutionTooHigh: "La resolución es demasiado alta", imageValidateSizeLabelExpectedMinResolution: "La resolución mínima es {minResolution}", imageValidateSizeLabelExpectedMaxResolution: "La resolución máxima es {maxResolution}", }; ================================================ FILE: locale/et-ee.js ================================================ export default { labelIdle: 'Lohista oma failid siia või Sirvi ', labelInvalidField: 'Väli sisaldab kehtetuid faile', labelFileWaitingForSize: 'Ootab suurust', labelFileSizeNotAvailable: 'Suurus pole saadaval', labelFileLoading: 'Laadimine', labelFileLoadError: 'Viga laadimisel', labelFileProcessing: 'Üleslaadimine', labelFileProcessingComplete: 'Üleslaadimine lõpetatud', labelFileProcessingAborted: 'Üleslaadimine tühistatud', labelFileProcessingError: 'Viga üleslaadimisel', labelFileProcessingRevertError: 'Viga tagasivõtmisel', labelFileRemoveError: 'Viga eemaldamisel', labelTapToCancel: 'katkesta puudutades', labelTapToRetry: 'proovi uuesti puudutades', labelTapToUndo: 'võta tagasi puudutades', labelButtonRemoveItem: 'Eemalda', labelButtonAbortItemLoad: 'Katkesta', labelButtonRetryItemLoad: 'Proovi uuesti', labelButtonAbortItemProcessing: 'Tühista', labelButtonUndoItemProcessing: 'Võta tagasi', labelButtonRetryItemProcessing: 'Proovi uuesti', labelButtonProcessItem: 'Lae üles', labelMaxFileSizeExceeded: 'Fail on liiga suur', labelMaxFileSize: 'Maksimaalne faili suurus on {filesize}', labelMaxTotalFileSizeExceeded: 'Maksimaalne kogusuurus ületatud', labelMaxTotalFileSize: 'Maksimaalne kogu faili suurus on {filesize}', labelFileTypeNotAllowed: 'Keelatud failitüüp', fileValidateTypeLabelExpectedTypes: 'Oodatakse {allButLastType} või {lastType}', imageValidateSizeLabelFormatError: 'Pildi formaat ei ole toetatud', imageValidateSizeLabelImageSizeTooSmall: 'Pilt on liiga väike', imageValidateSizeLabelImageSizeTooBig: 'Pilt on liiga suur', imageValidateSizeLabelExpectedMinSize: 'Minimaalne suurus on {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maksimaalne suurus on {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Resolutsioon on liiga madal', imageValidateSizeLabelImageResolutionTooHigh: 'Resolutsioon on liiga kõrge', imageValidateSizeLabelExpectedMinResolution: 'Minimaalne resolutsioon on {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maksimaalne resolutsioon on {maxResolution}' }; ================================================ FILE: locale/fa_ir.js ================================================ export default { labelIdle: 'فایل را اینجا بکشید و رها کنید، یا جستجو کنید ', labelInvalidField: 'فیلد دارای فایل های نامعتبر است', labelFileWaitingForSize: 'Waiting for size', labelFileSizeNotAvailable: 'حجم فایل مجاز نیست', labelFileLoading: 'درحال بارگذاری', labelFileLoadError: 'خطا در زمان اجرا', labelFileProcessing: 'درحال بارگذاری', labelFileProcessingComplete: 'بارگذاری کامل شد', labelFileProcessingAborted: 'بارگذاری لغو شد', labelFileProcessingError: 'خطا در زمان بارگذاری', labelFileProcessingRevertError: 'خطا در زمان حذف', labelFileRemoveError: 'خطا در زمان حذف', labelTapToCancel: 'برای لغو ضربه بزنید', labelTapToRetry: 'برای تکرار کلیک کنید', labelTapToUndo: 'برای برگشت کلیک کنید', labelButtonRemoveItem: 'حذف', labelButtonAbortItemLoad: 'لغو', labelButtonRetryItemLoad: 'تکرار', labelButtonAbortItemProcessing: 'لغو', labelButtonUndoItemProcessing: 'برگشت', labelButtonRetryItemProcessing: 'تکرار', labelButtonProcessItem: 'بارگذاری', labelMaxFileSizeExceeded: 'فایل بسیار حجیم است', labelMaxFileSize: 'حداکثر مجاز فایل {filesize} است', labelMaxTotalFileSizeExceeded: 'از حداکثر حجم فایل بیشتر شد', labelMaxTotalFileSize: 'حداکثر حجم فایل {filesize} است', labelFileTypeNotAllowed: 'نوع فایل نامعتبر است', fileValidateTypeLabelExpectedTypes: 'در انتظار {allButLastType} یا {lastType}', imageValidateSizeLabelFormatError: 'فرمت تصویر پشتیبانی نمی شود', imageValidateSizeLabelImageSizeTooSmall: 'تصویر بسیار کوچک است', imageValidateSizeLabelImageSizeTooBig: 'تصویر بسیار بزرگ است', imageValidateSizeLabelExpectedMinSize: 'حداقل اندازه {minWidth} × {minHeight} است', imageValidateSizeLabelExpectedMaxSize: 'حداکثر اندازه {maxWidth} × {maxHeight} است', imageValidateSizeLabelImageResolutionTooLow: 'وضوح تصویر بسیار کم است', imageValidateSizeLabelImageResolutionTooHigh: 'وضوع تصویر بسیار زیاد است', imageValidateSizeLabelExpectedMinResolution: 'حداقل وضوح تصویر {minResolution} است', imageValidateSizeLabelExpectedMaxResolution: 'حداکثر وضوح تصویر {maxResolution} است' }; ================================================ FILE: locale/fi-fi.js ================================================ export default { labelIdle: 'Vedä ja pudota tiedostoja tai Selaa ', labelInvalidField: 'Kentässä on virheellisiä tiedostoja', labelFileWaitingForSize: 'Odotetaan kokoa', labelFileSizeNotAvailable: 'Kokoa ei saatavilla', labelFileLoading: 'Ladataan', labelFileLoadError: 'Virhe latauksessa', labelFileProcessing: 'Lähetetään', labelFileProcessingComplete: 'Lähetys valmis', labelFileProcessingAborted: 'Lähetys peruttu', labelFileProcessingError: 'Virhe lähetyksessä', labelFileProcessingRevertError: 'Virhe palautuksessa', labelFileRemoveError: 'Virhe poistamisessa', labelTapToCancel: 'peruuta napauttamalla', labelTapToRetry: 'yritä uudelleen napauttamalla', labelTapToUndo: 'kumoa napauttamalla', labelButtonRemoveItem: 'Poista', labelButtonAbortItemLoad: 'Keskeytä', labelButtonRetryItemLoad: 'Yritä uudelleen', labelButtonAbortItemProcessing: 'Peruuta', labelButtonUndoItemProcessing: 'Kumoa', labelButtonRetryItemProcessing: 'Yritä uudelleen', labelButtonProcessItem: 'Lähetä', labelMaxFileSizeExceeded: 'Tiedoston koko on liian suuri', labelMaxFileSize: 'Tiedoston maksimikoko on {filesize}', labelMaxTotalFileSizeExceeded: 'Tiedostojen yhdistetty maksimikoko ylitetty', labelMaxTotalFileSize: 'Tiedostojen yhdistetty maksimikoko on {filesize}', labelFileTypeNotAllowed: 'Tiedostotyyppiä ei sallita', fileValidateTypeLabelExpectedTypes: 'Sallitaan {allButLastType} tai {lastType}', imageValidateSizeLabelFormatError: 'Kuvatyyppiä ei tueta', imageValidateSizeLabelImageSizeTooSmall: 'Kuva on liian pieni', imageValidateSizeLabelImageSizeTooBig: 'Kuva on liian suuri', imageValidateSizeLabelExpectedMinSize: 'Minimikoko on {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maksimikoko on {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Resoluutio on liian pieni', imageValidateSizeLabelImageResolutionTooHigh: 'Resoluutio on liian suuri', imageValidateSizeLabelExpectedMinResolution: 'Minimiresoluutio on {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maksimiresoluutio on {maxResolution}' }; ================================================ FILE: locale/fr-fr.js ================================================ export default { labelIdle: 'Faites glisser vos fichiers ou Parcourir ', labelInvalidField: "Le champ contient des fichiers invalides", labelFileWaitingForSize: "En attente de taille", labelFileSizeNotAvailable: "Taille non disponible", labelFileLoading: "Chargement", labelFileLoadError: "Erreur durant le chargement", labelFileProcessing: "Traitement", labelFileProcessingComplete: "Traitement effectué", labelFileProcessingAborted: "Traitement interrompu", labelFileProcessingError: "Erreur durant le traitement", labelFileProcessingRevertError: "Erreur durant la restauration", labelFileRemoveError: "Erreur durant la suppression", labelTapToCancel: "appuyer pour annuler", labelTapToRetry: "appuyer pour réessayer", labelTapToUndo: "appuyer pour revenir en arrière", labelButtonRemoveItem: "Retirer", labelButtonAbortItemLoad: "Annuler", labelButtonRetryItemLoad: "Recommencer", labelButtonAbortItemProcessing: "Annuler", labelButtonUndoItemProcessing: "Revenir en arrière", labelButtonRetryItemProcessing: "Recommencer", labelButtonProcessItem: "Transférer", labelMaxFileSizeExceeded: "Le fichier est trop volumineux", labelMaxFileSize: "La taille maximale de fichier est {filesize}", labelMaxTotalFileSizeExceeded: "Taille totale maximale dépassée", labelMaxTotalFileSize: "La taille totale maximale des fichiers est {filesize}", labelFileTypeNotAllowed: "Fichier non valide", fileValidateTypeLabelExpectedTypes: "Attendu {allButLastType} ou {lastType}", imageValidateSizeLabelFormatError: "Type d'image non pris en charge", imageValidateSizeLabelImageSizeTooSmall: "L'image est trop petite", imageValidateSizeLabelImageSizeTooBig: "L'image est trop grande", imageValidateSizeLabelExpectedMinSize: "La taille minimale est {minWidth} × {minHeight}", imageValidateSizeLabelExpectedMaxSize: "La taille maximale est {maxWidth} × {maxHeight}", imageValidateSizeLabelImageResolutionTooLow: "La résolution est trop faible", imageValidateSizeLabelImageResolutionTooHigh: "La résolution est trop élevée", imageValidateSizeLabelExpectedMinResolution: "La résolution minimale est {minResolution}", imageValidateSizeLabelExpectedMaxResolution: "La résolution maximale est {maxResolution}", }; ================================================ FILE: locale/he-he.js ================================================ export default { labelIdle: 'גרור ושחרר את הקבצים כאן או לחץ כאן לבחירה ', labelInvalidField: 'קובץ לא חוקי', labelFileWaitingForSize: 'מחשב את גודל הקבצים', labelFileSizeNotAvailable: 'לא ניתן לקבוע את גודל הקבצים', labelFileLoading: 'טוען...', labelFileLoadError: 'שגיאה ארעה בעת טעינת הקבצים', labelFileProcessing: 'מעלה את הקבצים', labelFileProcessingComplete: 'העלאת הקבצים הסתיימה', labelFileProcessingAborted: 'העלאת הקבצים בוטלה', labelFileProcessingError: 'שגיאה ארעה בעת העלאת הקבצים', labelFileProcessingRevertError: 'שגיאה ארעה בעת שחזור הקבצים', labelFileRemoveError: 'שגיאה ארעה בעת הסרת הקובץ', labelTapToCancel: 'הקלק לביטול', labelTapToRetry: 'הקלק לנסות שנית', labelTapToUndo: 'הקלק לשחזר', labelButtonRemoveItem: 'הסר', labelButtonAbortItemLoad: 'בטל', labelButtonRetryItemLoad: 'טען שנית', labelButtonAbortItemProcessing: 'בטל', labelButtonUndoItemProcessing: 'שחזר', labelButtonRetryItemProcessing: 'נסה שנית', labelButtonProcessItem: 'העלה קובץ', labelMaxFileSizeExceeded: 'הקובץ גדול מדי', labelMaxFileSize: 'גודל המירבי המותר הוא: {filesize}', labelMaxTotalFileSizeExceeded: 'גודל הקבצים חורג מהכמות המותרת', labelMaxTotalFileSize: 'הגודל המירבי של סך הקבצים: {filesize}', labelFileTypeNotAllowed: 'קובץ מסוג זה אינו מותר', fileValidateTypeLabelExpectedTypes: 'הקבצים המותרים הם {allButLastType} או {lastType}', imageValidateSizeLabelFormatError: 'תמונה בפורמט זה אינה נתמכת', imageValidateSizeLabelImageSizeTooSmall: 'תמונה זו קטנה מדי', imageValidateSizeLabelImageSizeTooBig: 'תמונה זו גדולה מדי', imageValidateSizeLabelExpectedMinSize: 'הגודל צריך להיות לפחות: {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'הגודל המרבי המותר: {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'הרזולוציה של תמונה זו נמוכה מדי', imageValidateSizeLabelImageResolutionTooHigh: 'הרזולוציה של תמונה זו גבוהה מדי', imageValidateSizeLabelExpectedMinResolution: 'הרזולוציה צריכה להיות לפחות: {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'הרזולוציה המירבית המותרת היא: {maxResolution}', }; ================================================ FILE: locale/hr-hr.js ================================================ export default { labelIdle: 'Ovdje "ispusti" datoteku ili Pretraži ', labelInvalidField: 'Polje sadrži neispravne datoteke', labelFileWaitingForSize: 'Čekanje na veličinu datoteke', labelFileSizeNotAvailable: 'Veličina datoteke nije dostupna', labelFileLoading: 'Učitavanje', labelFileLoadError: 'Greška tijekom učitavanja', labelFileProcessing: 'Prijenos', labelFileProcessingComplete: 'Prijenos završen', labelFileProcessingAborted: 'Prijenos otkazan', labelFileProcessingError: 'Greška tijekom prijenosa', labelFileProcessingRevertError: 'Greška tijekom vraćanja', labelFileRemoveError: 'Greška tijekom uklananja datoteke', labelTapToCancel: 'Dodirni za prekid', labelTapToRetry: 'Dodirni za ponovno', labelTapToUndo: 'Dodirni za vraćanje', labelButtonRemoveItem: 'Ukloni', labelButtonAbortItemLoad: 'Odbaci', labelButtonRetryItemLoad: 'Ponovi', labelButtonAbortItemProcessing: 'Prekini', labelButtonUndoItemProcessing: 'Vrati', labelButtonRetryItemProcessing: 'Ponovi', labelButtonProcessItem: 'Prijenos', labelMaxFileSizeExceeded: 'Datoteka je prevelika', labelMaxFileSize: 'Maksimalna veličina datoteke je {filesize}', labelMaxTotalFileSizeExceeded: 'Maksimalna ukupna veličina datoteke prekoračena', labelMaxTotalFileSize: 'Maksimalna ukupna veličina datoteke je {filesize}', labelFileTypeNotAllowed: 'Tip datoteke nije podržan', fileValidateTypeLabelExpectedTypes: 'Očekivan {allButLastType} ili {lastType}', imageValidateSizeLabelFormatError: 'Tip slike nije podržan', imageValidateSizeLabelImageSizeTooSmall: 'Slika je premala', imageValidateSizeLabelImageSizeTooBig: 'Slika je prevelika', imageValidateSizeLabelExpectedMinSize: 'Minimalna veličina je {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maksimalna veličina je {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Rezolucija je preniska', imageValidateSizeLabelImageResolutionTooHigh: 'Rezolucija je previsoka', imageValidateSizeLabelExpectedMinResolution: 'Minimalna rezolucija je {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maksimalna rezolucija je {maxResolution}' }; ================================================ FILE: locale/hu-hu.js ================================================ export default { labelIdle: 'Mozgasd ide a fájlt a feltöltéshez, vagy tallózás ', labelInvalidField: 'A mező érvénytelen fájlokat tartalmaz', labelFileWaitingForSize: 'Fáljméret kiszámolása', labelFileSizeNotAvailable: 'A fájlméret nem elérhető', labelFileLoading: 'Töltés', labelFileLoadError: 'Hiba a betöltés során', labelFileProcessing: 'Feltöltés', labelFileProcessingComplete: 'Sikeres feltöltés', labelFileProcessingAborted: 'A feltöltés megszakítva', labelFileProcessingError: 'Hiba történt a feltöltés során', labelFileProcessingRevertError: 'Hiba a visszaállítás során', labelFileRemoveError: 'Hiba történt az eltávolítás során', labelTapToCancel: 'koppints a törléshez', labelTapToRetry: 'koppints az újrakezdéshez', labelTapToUndo: 'koppints a visszavonáshoz', labelButtonRemoveItem: 'Eltávolítás', labelButtonAbortItemLoad: 'Megszakítás', labelButtonRetryItemLoad: 'Újrapróbálkozás', labelButtonAbortItemProcessing: 'Megszakítás', labelButtonUndoItemProcessing: 'Visszavonás', labelButtonRetryItemProcessing: 'Újrapróbálkozás', labelButtonProcessItem: 'Feltöltés', labelMaxFileSizeExceeded: 'A fájl túllépte a maximális méretet', labelMaxFileSize: 'Maximális fájlméret: {filesize}', labelMaxTotalFileSizeExceeded: 'Túllépte a maximális teljes méretet', labelMaxTotalFileSize: 'A maximáis teljes fájlméret: {filesize}', labelFileTypeNotAllowed: 'Érvénytelen típusú fájl', fileValidateTypeLabelExpectedTypes: 'Engedélyezett típusok {allButLastType} vagy {lastType}', imageValidateSizeLabelFormatError: 'A képtípus nem támogatott', imageValidateSizeLabelImageSizeTooSmall: 'A kép túl kicsi', imageValidateSizeLabelImageSizeTooBig: 'A kép túl nagy', imageValidateSizeLabelExpectedMinSize: 'Minimum méret: {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maximum méret: {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'A felbontás túl alacsony', imageValidateSizeLabelImageResolutionTooHigh: 'A felbontás túl magas', imageValidateSizeLabelExpectedMinResolution: 'Minimáis felbontás: {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maximális felbontás: {maxResolution}' }; ================================================ FILE: locale/id-id.js ================================================ export default { labelIdle: 'Seret & Jatuhkan berkas Anda atau Jelajahi', labelInvalidField: 'Isian berisi berkas yang tidak valid', labelFileWaitingForSize: 'Menunggu ukuran berkas', labelFileSizeNotAvailable: 'Ukuran berkas tidak tersedia', labelFileLoading: 'Memuat', labelFileLoadError: 'Kesalahan saat memuat', labelFileProcessing: 'Mengunggah', labelFileProcessingComplete: 'Pengunggahan selesai', labelFileProcessingAborted: 'Pengunggahan dibatalkan', labelFileProcessingError: 'Kesalahan saat pengunggahan', labelFileProcessingRevertError: 'Kesalahan saat pemulihan', labelFileRemoveError: 'Kesalahan saat penghapusan', labelTapToCancel: 'ketuk untuk membatalkan', labelTapToRetry: 'ketuk untuk mencoba lagi', labelTapToUndo: 'ketuk untuk mengurungkan', labelButtonRemoveItem: 'Hapus', labelButtonAbortItemLoad: 'Batalkan', labelButtonRetryItemLoad: 'Coba Kembali', labelButtonAbortItemProcessing: 'Batalkan', labelButtonUndoItemProcessing: 'Urungkan', labelButtonRetryItemProcessing: 'Coba Kembali', labelButtonProcessItem: 'Unggah', labelMaxFileSizeExceeded: 'Berkas terlalu besar', labelMaxFileSize: 'Ukuran berkas maksimum adalah {filesize}', labelMaxTotalFileSizeExceeded: 'Jumlah berkas maksimum terlampaui', labelMaxTotalFileSize: 'Jumlah berkas maksimum adalah {filesize}', labelFileTypeNotAllowed: 'Jenis berkas tidak valid', fileValidateTypeLabelExpectedTypes: 'Mengharapkan {allButLastType} atau {lastType}', imageValidateSizeLabelFormatError: 'Jenis citra tidak didukung', imageValidateSizeLabelImageSizeTooSmall: 'Citra terlalu kecil', imageValidateSizeLabelImageSizeTooBig: 'Citra terlalu besar', imageValidateSizeLabelExpectedMinSize: 'Ukuran minimum adalah {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Ukuran maksimum adalah {minWidth} × {minHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Resolusi terlalu rendah', imageValidateSizeLabelImageResolutionTooHigh: 'Resolusi terlalu tinggi', imageValidateSizeLabelExpectedMinResolution: 'Resolusi minimum adalah {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Resolusi maksimum adalah {maxResolution}' }; ================================================ FILE: locale/it-it.js ================================================ export default { labelIdle: 'Trascina e rilascia i tuoi file oppure Sfoglia ', labelInvalidField: "Il campo contiene dei file non validi", labelFileWaitingForSize: "In attesa della dimensione", labelFileSizeNotAvailable: "Dimensione non disponibile", labelFileLoading: "Caricamento", labelFileLoadError: "Errore durante il caricamento", labelFileProcessing: "Caricamento", labelFileProcessingComplete: "Caricamento completato", labelFileProcessingAborted: "Caricamento cancellato", labelFileProcessingError: "Errore durante il caricamento", labelFileProcessingRevertError: "Errore durante il ripristino", labelFileRemoveError: "Errore durante l'eliminazione", labelTapToCancel: "tocca per cancellare", labelTapToRetry: "tocca per riprovare", labelTapToUndo: "tocca per ripristinare", labelButtonRemoveItem: "Elimina", labelButtonAbortItemLoad: "Cancella", labelButtonRetryItemLoad: "Ritenta", labelButtonAbortItemProcessing: "Cancella", labelButtonUndoItemProcessing: "Indietro", labelButtonRetryItemProcessing: "Ritenta", labelButtonProcessItem: "Carica", labelMaxFileSizeExceeded: "La dimensione del file è eccessiva", labelMaxFileSize: "La dimensione massima del file è {filesize}", labelMaxTotalFileSizeExceeded: "Dimensione totale massima superata", labelMaxTotalFileSize: "La dimensione massima totale dei file è {filesize}", labelFileTypeNotAllowed: "File non supportato", fileValidateTypeLabelExpectedTypes: "Aspetta {allButLastType} o {lastType}", imageValidateSizeLabelFormatError: "Tipo di immagine non supportata", imageValidateSizeLabelImageSizeTooSmall: "L'immagine è troppo piccola", imageValidateSizeLabelImageSizeTooBig: "L'immagine è troppo grande", imageValidateSizeLabelExpectedMinSize: "La dimensione minima è {minWidth} × {minHeight}", imageValidateSizeLabelExpectedMaxSize: "La dimensione massima è {maxWidth} × {maxHeight}", imageValidateSizeLabelImageResolutionTooLow: "La risoluzione è troppo bassa", imageValidateSizeLabelImageResolutionTooHigh: "La risoluzione è troppo alta", imageValidateSizeLabelExpectedMinResolution: "La risoluzione minima è {minResolution}", imageValidateSizeLabelExpectedMaxResolution: "La risoluzione massima è {maxResolution}", }; ================================================ FILE: locale/ja-ja.js ================================================ export default { labelIdle: 'ファイルをドラッグ&ドロップ又はファイル選択', labelInvalidField: "アップロードできないファイルが含まれています", labelFileWaitingForSize: "ファイルサイズを待っています", labelFileSizeNotAvailable: "ファイルサイズがみつかりません", labelFileLoading: "読込中...", labelFileLoadError: "読込中にエラーが発生", labelFileProcessing: "読込中...", labelFileProcessingComplete: "アップロード完了", labelFileProcessingAborted: "アップロードがキャンセルされました", labelFileProcessingError: "アップロード中にエラーが発生", labelFileProcessingRevertError: "ロールバック中にエラーが発生", labelFileRemoveError: "削除中にエラーが発生", labelTapToCancel: "クリックしてキャンセル", labelTapToRetry: "クリックしてもう一度お試し下さい", labelTapToUndo: "元に戻すにはタップします", labelButtonRemoveItem: "削除", labelButtonAbortItemLoad: "中断", labelButtonRetryItemLoad: "もう一度実行", labelButtonAbortItemProcessing: "キャンセル", labelButtonUndoItemProcessing: "元に戻す", labelButtonRetryItemProcessing: "もう一度実行", labelButtonProcessItem: "アップロード", labelMaxFileSizeExceeded: "ファイルサイズが大きすぎます", labelMaxFileSize: "最大ファイルサイズは {filesize} です", labelMaxTotalFileSizeExceeded: "最大合計サイズを超えました", labelMaxTotalFileSize: "最大合計ファイルサイズは {filesize} です", labelFileTypeNotAllowed: "無効なファイルです", fileValidateTypeLabelExpectedTypes: "サポートしているファイルは {allButLastType} 又は {lastType} です", imageValidateSizeLabelFormatError: "サポートしていない画像です", imageValidateSizeLabelImageSizeTooSmall: "画像が小さすぎます", imageValidateSizeLabelImageSizeTooBig: "画像が大きすぎます", imageValidateSizeLabelExpectedMinSize: "画像の最小サイズは{minWidth}×{minHeight}です", imageValidateSizeLabelExpectedMaxSize: "画像の最大サイズは{maxWidth} × {maxHeight}です", imageValidateSizeLabelImageResolutionTooLow: "画像の解像度が低すぎます", imageValidateSizeLabelImageResolutionTooHigh: "画像の解像度が高すぎます", imageValidateSizeLabelExpectedMinResolution: "画像の最小解像度は{minResolution}です", imageValidateSizeLabelExpectedMaxResolution: "画像の最大解像度は{maxResolution}です", }; ================================================ FILE: locale/km-km.js ================================================ export default { labelIdle: 'ទាញ&ដាក់ហ្វាល់ឯកសាររបស់អ្នក ឬ ស្វែងរក ', labelInvalidField: 'ចន្លោះមានឯកសារមិនត្រឹមត្រូវ', labelFileWaitingForSize: 'កំពុងរង់ចាំទំហំ', labelFileSizeNotAvailable: 'ទំហំមិនអាចប្រើបាន', labelFileLoading: 'កំពុងដំណើរការ', labelFileLoadError: 'មានបញ្ហាកំឡុងពេលដំណើរការ', labelFileProcessing: 'កំពុងផ្ទុកឡើង', labelFileProcessingComplete: 'ការផ្ទុកឡើងពេញលេញ', labelFileProcessingAborted: 'ការបង្ហោះត្រូវបានបោះបង់', labelFileProcessingError: 'មានបញ្ហាកំឡុងពេលកំពុងផ្ទុកឡើង', labelFileProcessingRevertError: 'មានបញ្ហាកំឡុងពេលត្រឡប់', labelFileRemoveError: 'មានបញ្ហាកំឡុងពេលដកចេញ', labelTapToCancel: 'ចុចដើម្បីបោះបង់', labelTapToRetry: 'ចុចដើម្បីព្យាយាមម្តងទៀត', labelTapToUndo: 'ចុចដើម្បីមិនធ្វើវិញ', labelButtonRemoveItem: 'យកចេញ', labelButtonAbortItemLoad: 'បោះបង់', labelButtonRetryItemLoad: 'ព្យាយាមម្តងទៀត', labelButtonAbortItemProcessing: 'បោះបង់', labelButtonUndoItemProcessing: 'មិនធ្វើវិញ', labelButtonRetryItemProcessing: 'ព្យាយាមម្តងទៀត', labelButtonProcessItem: 'ផ្ទុកឡើង', labelMaxFileSizeExceeded: 'ឯកសារធំពេក', labelMaxFileSize: 'ទំហំឯកសារអតិបរមាគឺ {filesize}', labelMaxTotalFileSizeExceeded: 'លើសទំហំសរុបអតិបរមា', labelMaxTotalFileSize: 'ទំហំឯកសារសរុបអតិបរមាគឺ {filesize}', labelFileTypeNotAllowed: 'ប្រភេទឯកសារមិនត្រឹមត្រូវ', fileValidateTypeLabelExpectedTypes: 'រំពឹងថា {allButLastType} ឬ {lastType}', imageValidateSizeLabelFormatError: 'ប្រភេទរូបភាពមិនត្រឹមត្រូវ', imageValidateSizeLabelImageSizeTooSmall: 'រូបភាពតូចពេក', imageValidateSizeLabelImageSizeTooBig: 'រូបភាពធំពេក', imageValidateSizeLabelExpectedMinSize: 'ទំហំអប្បបរមាគឺ {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'ទំហំអតិបរមាគឺ {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'គុណភាពបង្ហាញទាបពេក', imageValidateSizeLabelImageResolutionTooHigh: 'គុណភាពបង្ហាញខ្ពស់ពេក', imageValidateSizeLabelExpectedMinResolution: 'គុណភាពបង្ហាញអប្បបរមាគឺ {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'គុណភាពបង្ហាញអតិបរមាគឺ {maxResolution}' }; ================================================ FILE: locale/ko-kr.js ================================================ export default { labelIdle: '파일을 드래그 하거나 찾아보기 ', labelInvalidField: '필드에 유효하지 않은 파일이 있습니다.', labelFileWaitingForSize: '용량 확인중', labelFileSizeNotAvailable: '사용할 수 없는 용량', labelFileLoading: '불러오는 중', labelFileLoadError: '파일 불러오기 실패', labelFileProcessing: '업로드 중', labelFileProcessingComplete: '업로드 성공', labelFileProcessingAborted: '업로드 취소됨', labelFileProcessingError: '파일 업로드 실패', labelFileProcessingRevertError: '되돌리기 실패', labelFileRemoveError: '제거 실패', labelTapToCancel: '탭하여 취소', labelTapToRetry: '탭하여 재시작', labelTapToUndo: '탭하여 실행 취소', labelButtonRemoveItem: '제거', labelButtonAbortItemLoad: '중단', labelButtonRetryItemLoad: '재시작', labelButtonAbortItemProcessing: '취소', labelButtonUndoItemProcessing: '실행 취소', labelButtonRetryItemProcessing: '재시작', labelButtonProcessItem: '업로드', labelMaxFileSizeExceeded: '파일이 너무 큽니다.', labelMaxFileSize: '최대 파일 용량은 {filesize} 입니다.', labelMaxTotalFileSizeExceeded: '최대 전체 파일 용량 초과하였습니다.', labelMaxTotalFileSize: '최대 전체 파일 용량은 {filesize} 입니다.', labelFileTypeNotAllowed: '잘못된 형식의 파일', fileValidateTypeLabelExpectedTypes: '{allButLastType} 또는 {lastType}', imageValidateSizeLabelFormatError: '지원되지 않는 이미지 유형', imageValidateSizeLabelImageSizeTooSmall: '이미지가 너무 작습니다.', imageValidateSizeLabelImageSizeTooBig: '이미지가 너무 큽니다.', imageValidateSizeLabelExpectedMinSize: '이미지 최소 크기는 {minWidth} × {minHeight} 입니다', imageValidateSizeLabelExpectedMaxSize: '이미지 최대 크기는 {maxWidth} × {maxHeight} 입니다', imageValidateSizeLabelImageResolutionTooLow: '해상도가 너무 낮습니다.', imageValidateSizeLabelImageResolutionTooHigh: '해상도가 너무 높습니다.', imageValidateSizeLabelExpectedMinResolution: '최소 해상도는 {minResolution} 입니다.', imageValidateSizeLabelExpectedMaxResolution: '최대 해상도는 {maxResolution} 입니다.' }; ================================================ FILE: locale/ku-ckb.js ================================================ export default { labelIdle: 'پەڕگەکان فڕێ بدە ئێرە بۆ بارکردن یان هەڵبژێرە ', labelInvalidField: 'پەڕگەی نادروستی تێدایە', labelFileWaitingForSize: 'چاوەڕوانیی قەبارە', labelFileSizeNotAvailable: 'قەبارە بەردەست نیە', labelFileLoading: 'بارکردن', labelFileLoadError: 'هەڵە لەماوەی بارکردن', labelFileProcessing: 'بارکردن', labelFileProcessingComplete: 'بارکردن تەواو بوو', labelFileProcessingAborted: 'بارکردن هەڵوەشایەوە', labelFileProcessingError: 'هەڵە لەکاتی بارکردندا', labelFileProcessingRevertError: 'هەڵە لە کاتی گەڕانەوە', labelFileRemoveError: 'هەڵە لە کاتی سڕینەوە', labelTapToCancel: 'بۆ هەڵوەشاندنەوە Tab دابگرە', labelTapToRetry: 'tap دابگرە بۆ دووبارەکردنەوە', labelTapToUndo: 'tap دابگرە بۆ گەڕاندنەوە', labelButtonRemoveItem: 'سڕینەوە', labelButtonAbortItemLoad: 'هەڵوەشاندنەوە', labelButtonRetryItemLoad: 'هەوڵدانەوە', labelButtonAbortItemProcessing: 'پەشیمانبوونەوە', labelButtonUndoItemProcessing: 'گەڕاندنەوە', labelButtonRetryItemProcessing: 'هەوڵدانەوە', labelButtonProcessItem: 'بارکردن', labelMaxFileSizeExceeded: 'پەڕگە زۆر گەورەیە', labelMaxFileSize: 'زۆرترین قەبارە {filesize}', labelMaxTotalFileSizeExceeded: 'زۆرترین قەبارەی کۆی گشتی تێپەڕێندرا', labelMaxTotalFileSize: 'زۆرترین قەبارەی کۆی پەڕگە {filesize}', labelFileTypeNotAllowed: 'جۆری پەڕگەکە نادروستە', fileValidateTypeLabelExpectedTypes: 'جگە لە {allButLastType} یان {lastType}', imageValidateSizeLabelFormatError: 'جۆری وێنە پاڵپشتیی نەکراوە', imageValidateSizeLabelImageSizeTooSmall: 'وێنەکە زۆر بچووکە', imageValidateSizeLabelImageSizeTooBig: 'وێنەکە زۆر گەورەیە', imageValidateSizeLabelExpectedMinSize: 'کەمترین قەبارە {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'زۆرترین قەبارە {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'وردبینییەکەی زۆر کەمە', imageValidateSizeLabelImageResolutionTooHigh: 'وردبینییەکەی زۆر بەرزە', imageValidateSizeLabelExpectedMinResolution: 'کەمترین وردبینیی {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'زۆرترین وردبینی {maxResolution}' }; ================================================ FILE: locale/kur-ckb.js ================================================ export default { labelIdle: 'فایلەکانت ڕابکێشە یان دیاری بکە ', labelInvalidField: "هەڵبژاردنەکان فایلی نادروستی تێدایە", labelFileWaitingForSize: "چاوەڕوانکردنی قەبارە", labelFileSizeNotAvailable: "قەبارە بەردەست نییە", labelFileLoading: "چاوەڕوان بە", labelFileLoadError: "هەڵەیەک ڕوویدا لە کاتی خوێندنەوە", labelFileProcessing: "بارکردن", labelFileProcessingComplete: "بارکردن سەرکەوتو بوو", labelFileProcessingAborted: "بارکردن پاشگەزکرایەوە", labelFileProcessingError: "هەڵەیەک ڕوویدا لەکاتی بارکردن", labelFileProcessingRevertError: "هەڵەیەک ڕوویدا لەکاتی گەڕاندنەوە", labelFileRemoveError: "هەڵەیەک ڕوویدا لەکاتی سڕینەوە", labelTapToCancel: "کلیک بۆ پاشگەزبوونەوە", labelTapToRetry: "کلیک بۆ دووبارەکردنەوە", labelTapToUndo: "کلیک بۆ گەڕاندنەوە", labelButtonRemoveItem: "سڕینەوە", labelButtonAbortItemLoad: "لەباربردن", labelButtonRetryItemLoad: "دووبارەکردنەوە", labelButtonAbortItemProcessing: "پاشگەزکردنەوە", labelButtonUndoItemProcessing: "گەڕاندنەوە", labelButtonRetryItemProcessing: "دووبارەکردنەوە", labelButtonProcessItem: "بارکردن", labelMaxFileSizeExceeded: "قەبارەی فایلەکە زۆرە", labelMaxFileSize: "قەبارەی فایل پێویستە لە {filesize} کەمتر بێت", labelMaxTotalFileSizeExceeded: "قەبارەی فایل زۆر زیاترە لە قەبارەی دیاریکراو", labelMaxTotalFileSize: "کۆی گشتی قەبارەی فایل پێویستە لە {filesize} کەمتر بێت", labelFileTypeNotAllowed: "جۆری فایل نادروستە", fileValidateTypeLabelExpectedTypes: "چاوەڕوانی {allButLastType} یان {lastType} کرا", imageValidateSizeLabelFormatError: "جۆری وێنە نادروستە", imageValidateSizeLabelImageSizeTooSmall: "وێنە زۆر بچووکە", imageValidateSizeLabelImageSizeTooBig: "وێنە زۆر گەورەیە", imageValidateSizeLabelExpectedMinSize: "کەمترین پانی و بەرزی پێویستە {minWidth} × {minHeight} بیت", imageValidateSizeLabelExpectedMaxSize: "زۆرترین پانی و بەرزی پێویستە {maxWidth} × {maxHeight} بێت", imageValidateSizeLabelImageResolutionTooLow: "چوارچێوەکە زۆر کەمە", imageValidateSizeLabelImageResolutionTooHigh: "چوارچێوەکە زۆر بەرزە", imageValidateSizeLabelExpectedMinResolution: "کەمترین چوارچێوە پێویستە {minResolution} بێت", imageValidateSizeLabelExpectedMaxResolution: "بەرزترین چوارچێوە پێویستە {maxResolution} بێت", }; ================================================ FILE: locale/lt-lt.js ================================================ export default { labelIdle: 'Įdėkite failus čia arba Ieškokite ', labelInvalidField: 'Laukelis talpina netinkamus failus', labelFileWaitingForSize: 'Laukiama dydžio', labelFileSizeNotAvailable: 'Dydis nežinomas', labelFileLoading: 'Kraunama', labelFileLoadError: 'Klaida įkeliant', labelFileProcessing: 'Įkeliama', labelFileProcessingComplete: 'Įkėlimas sėkmingas', labelFileProcessingAborted: 'Įkėlimas atšauktas', labelFileProcessingError: 'Įkeliant įvyko klaida', labelFileProcessingRevertError: 'Atšaukiant įvyko klaida', labelFileRemoveError: 'Ištrinant įvyko klaida', labelTapToCancel: 'Palieskite norėdami atšaukti', labelTapToRetry: 'Palieskite norėdami pakartoti', labelTapToUndo: 'Palieskite norėdami atšaukti', labelButtonRemoveItem: 'Ištrinti', labelButtonAbortItemLoad: 'Sustabdyti', labelButtonRetryItemLoad: 'Pakartoti', labelButtonAbortItemProcessing: 'Atšaukti', labelButtonUndoItemProcessing: 'Atšaukti', labelButtonRetryItemProcessing: 'Pakartoti', labelButtonProcessItem: 'Įkelti', labelMaxFileSizeExceeded: 'Failas per didelis', labelMaxFileSize: 'Maksimalus failo dydis yra {filesize}', labelMaxTotalFileSizeExceeded: 'Viršijote maksimalų leistiną dydį', labelMaxTotalFileSize: 'Maksimalus leistinas dydis yra {filesize}', labelFileTypeNotAllowed: 'Netinkamas failas', fileValidateTypeLabelExpectedTypes: 'Tikisi {allButLastType} arba {lastType}', imageValidateSizeLabelFormatError: 'Nuotraukos formatas nepalaikomas', imageValidateSizeLabelImageSizeTooSmall: 'Nuotrauka per maža', imageValidateSizeLabelImageSizeTooBig: 'Nuotrauka per didelė', imageValidateSizeLabelExpectedMinSize: 'Minimalus dydis yra {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maksimalus dydis yra {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Rezoliucija per maža', imageValidateSizeLabelImageResolutionTooHigh: 'Rezoliucija per didelė', imageValidateSizeLabelExpectedMinResolution: 'Minimali rezoliucija yra {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maksimali rezoliucija yra {maxResolution}' }; ================================================ FILE: locale/lus-lus.js ================================================ export default { labelIdle: 'I file hnûklût rawh, emaw Zawnna ', labelInvalidField: 'Hemi hian files diklo a kengtel', labelFileWaitingForSize: 'A lenzawng a nghâk mek', labelFileSizeNotAvailable: 'A lenzawng a awmlo', labelFileLoading: 'Loading', labelFileLoadError: 'Load laiin dik lo a awm', labelFileProcessing: 'Uploading', labelFileProcessingComplete: 'Upload a zo', labelFileProcessingAborted: 'Upload sût a ni', labelFileProcessingError: 'Upload laiin dik lo a awm', labelFileProcessingRevertError: 'Dahkîr laiin dik lo a awm', labelFileRemoveError: 'Paih laiin dik lo a awm', labelTapToCancel: 'Sût turin hmet rawh', labelTapToRetry: 'Tinawn turin hmet rawh', labelTapToUndo: 'Tilet turin hmet rawh', labelButtonRemoveItem: 'Paihna', labelButtonAbortItemLoad: 'Tihtlawlhna', labelButtonRetryItemLoad: 'Tihnawnna', labelButtonAbortItemProcessing: 'Sûtna', labelButtonUndoItemProcessing: 'Tihletna', labelButtonRetryItemProcessing: 'Tihnawnna', labelButtonProcessItem: 'Upload', labelMaxFileSizeExceeded: 'File a lian lutuk', labelMaxFileSize: 'File lenzawng tam ber chu {filesize} ani', labelMaxTotalFileSizeExceeded: 'A lenzawng belh khâwm tam ber a pêl', labelMaxTotalFileSize: 'File lenzawng belh khâwm tam ber chu {filesize} a ni', labelFileTypeNotAllowed: 'File type dik lo a ni', fileValidateTypeLabelExpectedTypes: '{allButLastType} emaw {lastType} emaw beisei a ni', imageValidateSizeLabelFormatError: 'Thlalâk type a thlâwplo', imageValidateSizeLabelImageSizeTooSmall: 'Thlalâk hi a tê lutuk', imageValidateSizeLabelImageSizeTooBig: 'Thlalâk hi a lian lutuk', imageValidateSizeLabelExpectedMinSize: 'A lenzawng tlêm ber chu {minWidth} x {minHeight} a ni', imageValidateSizeLabelExpectedMaxSize: 'A lenzawng tam ber chu {maxWidth} x {maxHeight} a ni', imageValidateSizeLabelImageResolutionTooLow: 'Resolution a hniam lutuk', imageValidateSizeLabelImageResolutionTooHigh: 'Resolution a sâng lutuk', imageValidateSizeLabelExpectedMinResolution: 'Resolution hniam ber chu {minResolution} a ni', imageValidateSizeLabelExpectedMaxResolution: 'Resolution sâng ber chu {maxResolution} a ni' }; ================================================ FILE: locale/lv-lv.js ================================================ export default { labelIdle: 'Ievelciet savus failus vai pārlūkojiet šeit ', labelInvalidField: 'Lauks satur nederīgus failus', labelFileWaitingForSize: 'Gaidām faila izmēru', labelFileSizeNotAvailable: 'Izmērs nav pieejams', labelFileLoading: 'Notiek ielāde', labelFileLoadError: 'Notika kļūda ielādes laikā', labelFileProcessing: 'Notiek augšupielāde', labelFileProcessingComplete: 'Augšupielāde pabeigta', labelFileProcessingAborted: 'Augšupielāde atcelta', labelFileProcessingError: 'Notika kļūda augšupielādes laikā', labelFileProcessingRevertError: 'Notika kļūda atgriešanas laikā', labelFileRemoveError: 'Notika kļūda dzēšanas laikā', labelTapToCancel: 'pieskarieties, lai atceltu', labelTapToRetry: 'pieskarieties, lai mēģinātu vēlreiz', labelTapToUndo: 'pieskarieties, lai atsauktu', labelButtonRemoveItem: 'Dzēst', labelButtonAbortItemLoad: 'Pārtraukt', labelButtonRetryItemLoad: 'Mēģināt vēlreiz', labelButtonAbortItemProcessing: 'Pārtraucam', labelButtonUndoItemProcessing: 'Atsaucam', labelButtonRetryItemProcessing: 'Mēģinām vēlreiz', labelButtonProcessItem: 'Augšupielādēt', labelMaxFileSizeExceeded: 'Fails ir pārāk liels', labelMaxFileSize: 'Maksimālais faila izmērs ir {filesize}', labelMaxTotalFileSizeExceeded: 'Pārsniegts maksimālais kopējais failu izmērs', labelMaxTotalFileSize: 'Maksimālais kopējais failu izmērs ir {filesize}', labelFileTypeNotAllowed: 'Nederīgs faila tips', fileValidateTypeLabelExpectedTypes: 'Sagaidām {allButLastType} vai {lastType}', imageValidateSizeLabelFormatError: 'Neatbilstošs attēla tips', imageValidateSizeLabelImageSizeTooSmall: 'Attēls ir pārāk mazs', imageValidateSizeLabelImageSizeTooBig: 'Attēls ir pārāk liels', imageValidateSizeLabelExpectedMinSize: 'Minimālais izmērs ir {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maksimālais izmērs ir {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Izšķirtspēja ir pārāk zema', imageValidateSizeLabelImageResolutionTooHigh: 'Izšķirtspēja ir pārāk augsta', imageValidateSizeLabelExpectedMinResolution: 'Minimālā izšķirtspēja ir {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maksimālā izšķirtspēja ir {maxResolution}' }; ================================================ FILE: locale/nl-nl.js ================================================ export default { labelIdle: 'Drag & Drop je bestanden of Bladeren ', labelInvalidField: 'Veld bevat ongeldige bestanden', labelFileWaitingForSize: 'Wachten op grootte', labelFileSizeNotAvailable: 'Grootte niet beschikbaar', labelFileLoading: 'Laden', labelFileLoadError: 'Fout tijdens laden', labelFileProcessing: 'Uploaden', labelFileProcessingComplete: 'Upload afgerond', labelFileProcessingAborted: 'Upload geannuleerd', labelFileProcessingError: 'Fout tijdens upload', labelFileProcessingRevertError: 'Fout bij herstellen', labelFileRemoveError: 'Fout bij verwijderen', labelTapToCancel: 'tik om te annuleren', labelTapToRetry: 'tik om opnieuw te proberen', labelTapToUndo: 'tik om ongedaan te maken', labelButtonRemoveItem: 'Verwijderen', labelButtonAbortItemLoad: 'Afbreken', labelButtonRetryItemLoad: 'Opnieuw proberen', labelButtonAbortItemProcessing: 'Annuleren', labelButtonUndoItemProcessing: 'Ongedaan maken', labelButtonRetryItemProcessing: 'Opnieuw proberen', labelButtonProcessItem: 'Upload', labelMaxFileSizeExceeded: 'Bestand is te groot', labelMaxFileSize: 'Maximale bestandsgrootte is {filesize}', labelMaxTotalFileSizeExceeded: 'Maximale totale grootte overschreden', labelMaxTotalFileSize: 'Maximale totale bestandsgrootte is {filesize}', labelFileTypeNotAllowed: 'Ongeldig bestandstype', fileValidateTypeLabelExpectedTypes: 'Verwacht {allButLastType} of {lastType}', imageValidateSizeLabelFormatError: 'Afbeeldingstype niet ondersteund', imageValidateSizeLabelImageSizeTooSmall: 'Afbeelding is te klein', imageValidateSizeLabelImageSizeTooBig: 'Afbeelding is te groot', imageValidateSizeLabelExpectedMinSize: 'Minimale afmeting is {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maximale afmeting is {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Resolutie is te laag', imageValidateSizeLabelImageResolutionTooHigh: 'Resolution is too high', imageValidateSizeLabelExpectedMinResolution: 'Minimale resolutie is {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maximale resolutie is {maxResolution}' }; ================================================ FILE: locale/no_nb.js ================================================ export default { labelIdle: 'Dra og slipp filene dine, eller Bla gjennom... ', labelInvalidField: 'Feltet inneholder ugyldige filer', labelFileWaitingForSize: 'Venter på størrelse', labelFileSizeNotAvailable: 'Størrelse ikke tilgjengelig', labelFileLoading: 'Laster', labelFileLoadError: 'Feil under lasting', labelFileProcessing: 'Laster opp', labelFileProcessingComplete: 'Opplasting ferdig', labelFileProcessingAborted: 'Opplasting avbrutt', labelFileProcessingError: 'Feil under opplasting', labelFileProcessingRevertError: 'Feil under reversering', labelFileRemoveError: 'Feil under flytting', labelTapToCancel: 'klikk for å avbryte', labelTapToRetry: 'klikk for å prøve på nytt', labelTapToUndo: 'klikk for å angre', labelButtonRemoveItem: 'Fjern', labelButtonAbortItemLoad: 'Avbryt', labelButtonRetryItemLoad: 'Prøv på nytt', labelButtonAbortItemProcessing: 'Avbryt', labelButtonUndoItemProcessing: 'Angre', labelButtonRetryItemProcessing: 'Prøv på nytt', labelButtonProcessItem: 'Last opp', labelMaxFileSizeExceeded: 'Filen er for stor', labelMaxFileSize: 'Maksimal filstørrelse er {filesize}', labelMaxTotalFileSizeExceeded: 'Maksimal total størrelse oversteget', labelMaxTotalFileSize: 'Maksimal total størrelse er {filesize}', labelFileTypeNotAllowed: 'Ugyldig filtype', fileValidateTypeLabelExpectedTypes: 'Forventer {allButLastType} eller {lastType}', imageValidateSizeLabelFormatError: 'Bildeformat ikke støttet', imageValidateSizeLabelImageSizeTooSmall: 'Bildet er for lite', imageValidateSizeLabelImageSizeTooBig: 'Bildet er for stort', imageValidateSizeLabelExpectedMinSize: 'Minimumsstørrelse er {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maksimumsstørrelse er {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Oppløsningen er for lav', imageValidateSizeLabelImageResolutionTooHigh: 'Oppløsningen er for høy', imageValidateSizeLabelExpectedMinResolution: 'Minimum oppløsning er {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maksimal oppløsning er {maxResolution}' }; ================================================ FILE: locale/pl-pl.js ================================================ export default { labelIdle: 'Przeciągnij i upuść lub wybierz pliki', labelInvalidField: 'Nieprawidłowe pliki', labelFileWaitingForSize: 'Pobieranie rozmiaru', labelFileSizeNotAvailable: 'Nieznany rozmiar', labelFileLoading: 'Wczytywanie', labelFileLoadError: 'Błąd wczytywania', labelFileProcessing: 'Przesyłanie', labelFileProcessingComplete: 'Przesłano', labelFileProcessingAborted: 'Przerwano', labelFileProcessingError: 'Przesyłanie nie powiodło się', labelFileProcessingRevertError: 'Coś poszło nie tak', labelFileRemoveError: 'Nieudane usunięcie', labelTapToCancel: 'Anuluj', labelTapToRetry: 'Ponów', labelTapToUndo: 'Cofnij', labelButtonRemoveItem: 'Usuń', labelButtonAbortItemLoad: 'Przerwij', labelButtonRetryItemLoad: 'Ponów', labelButtonAbortItemProcessing: 'Anuluj', labelButtonUndoItemProcessing: 'Cofnij', labelButtonRetryItemProcessing: 'Ponów', labelButtonProcessItem: 'Prześlij', labelMaxFileSizeExceeded: 'Plik jest zbyt duży', labelMaxFileSize: 'Dopuszczalna wielkość pliku to {filesize}', labelMaxTotalFileSizeExceeded: 'Przekroczono łączny rozmiar plików', labelMaxTotalFileSize: 'Łączny rozmiar plików nie może przekroczyć {filesize}', labelFileTypeNotAllowed: 'Niedozwolony rodzaj pliku', fileValidateTypeLabelExpectedTypes: 'Oczekiwano {allButLastType} lub {lastType}', imageValidateSizeLabelFormatError: 'Nieobsługiwany format obrazu', imageValidateSizeLabelImageSizeTooSmall: 'Obraz jest zbyt mały', imageValidateSizeLabelImageSizeTooBig: 'Obraz jest zbyt duży', imageValidateSizeLabelExpectedMinSize: 'Minimalne wymiary obrazu to {minWidth}×{minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maksymalna wymiary obrazu to {maxWidth}×{maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Rozdzielczość jest zbyt niska', imageValidateSizeLabelImageResolutionTooHigh: 'Rozdzielczość jest zbyt wysoka', imageValidateSizeLabelExpectedMinResolution: 'Minimalna rozdzielczość to {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maksymalna rozdzielczość to {maxResolution}' }; ================================================ FILE: locale/pt-br.js ================================================ export default { labelIdle: 'Arraste e solte os arquivos ou Clique aqui ', labelInvalidField: 'Arquivos inválidos', labelFileWaitingForSize: 'Calculando o tamanho do arquivo', labelFileSizeNotAvailable: 'Tamanho do arquivo indisponível', labelFileLoading: 'Carregando', labelFileLoadError: 'Erro durante o carregamento', labelFileProcessing: 'Enviando', labelFileProcessingComplete: 'Envio finalizado', labelFileProcessingAborted: 'Envio cancelado', labelFileProcessingError: 'Erro durante o envio', labelFileProcessingRevertError: 'Erro ao reverter o envio', labelFileRemoveError: 'Erro ao remover o arquivo', labelTapToCancel: 'clique para cancelar', labelTapToRetry: 'clique para reenviar', labelTapToUndo: 'clique para desfazer', labelButtonRemoveItem: 'Remover', labelButtonAbortItemLoad: 'Abortar', labelButtonRetryItemLoad: 'Reenviar', labelButtonAbortItemProcessing: 'Cancelar', labelButtonUndoItemProcessing: 'Desfazer', labelButtonRetryItemProcessing: 'Reenviar', labelButtonProcessItem: 'Enviar', labelMaxFileSizeExceeded: 'Arquivo é muito grande', labelMaxFileSize: 'O tamanho máximo permitido: {filesize}', labelMaxTotalFileSizeExceeded: 'Tamanho total dos arquivos excedido', labelMaxTotalFileSize: 'Tamanho total permitido: {filesize}', labelFileTypeNotAllowed: 'Tipo de arquivo inválido', fileValidateTypeLabelExpectedTypes: 'Tipos de arquivo suportados são {allButLastType} ou {lastType}', imageValidateSizeLabelFormatError: 'Tipo de imagem inválida', imageValidateSizeLabelImageSizeTooSmall: 'Imagem muito pequena', imageValidateSizeLabelImageSizeTooBig: 'Imagem muito grande', imageValidateSizeLabelExpectedMinSize: 'Tamanho mínimo permitida: {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Tamanho máximo permitido: {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Resolução muito baixa', imageValidateSizeLabelImageResolutionTooHigh: 'Resolução muito alta', imageValidateSizeLabelExpectedMinResolution: 'Resolução mínima permitida: {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Resolução máxima permitida: {maxResolution}', }; ================================================ FILE: locale/pt-pt.js ================================================ export default { labelIdle: 'Arraste & Largue os ficheiros ou Seleccione ', labelInvalidField: 'O campo contém ficheiros inválidos', labelFileWaitingForSize: 'A aguardar tamanho', labelFileSizeNotAvailable: 'Tamanho não disponível', labelFileLoading: 'A carregar', labelFileLoadError: 'Erro ao carregar', labelFileProcessing: 'A carregar', labelFileProcessingComplete: 'Carregamento completo', labelFileProcessingAborted: 'Carregamento cancelado', labelFileProcessingError: 'Erro ao carregar', labelFileProcessingRevertError: 'Erro ao reverter', labelFileRemoveError: 'Erro ao remover', labelTapToCancel: 'carregue para cancelar', labelTapToRetry: 'carregue para tentar novamente', labelTapToUndo: 'carregue para desfazer', labelButtonRemoveItem: 'Remover', labelButtonAbortItemLoad: 'Abortar', labelButtonRetryItemLoad: 'Tentar novamente', labelButtonAbortItemProcessing: 'Cancelar', labelButtonUndoItemProcessing: 'Desfazer', labelButtonRetryItemProcessing: 'Tentar novamente', labelButtonProcessItem: 'Carregar', labelMaxFileSizeExceeded: 'Ficheiro demasiado grande', labelMaxFileSize: 'O tamanho máximo do ficheiro é de {filesize}', labelMaxTotalFileSizeExceeded: 'Tamanho máximo total excedido', labelMaxTotalFileSize: 'O tamanho máximo total do ficheiro é de {filesize}', labelFileTypeNotAllowed: 'Tipo de ficheiro inválido', fileValidateTypeLabelExpectedTypes: 'É esperado {allButLastType} ou {lastType}', imageValidateSizeLabelFormatError: 'Tipo de imagem não suportada', imageValidateSizeLabelImageSizeTooSmall: 'A imagem é demasiado pequena', imageValidateSizeLabelImageSizeTooBig: 'A imagem é demasiado grande', imageValidateSizeLabelExpectedMinSize: 'O tamanho mínimo é de {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'O tamanho máximo é de {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'A resolução é demasiado baixa', imageValidateSizeLabelImageResolutionTooHigh: 'A resolução é demasiado grande', imageValidateSizeLabelExpectedMinResolution: 'A resolução mínima é de {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'A resolução máxima é de {maxResolution}' }; ================================================ FILE: locale/ro-ro.js ================================================ export default { labelIdle: 'Trage și plasează fișiere sau Caută-le ', labelInvalidField: 'Câmpul conține fișiere care nu sunt valide', labelFileWaitingForSize: 'În așteptarea dimensiunii', labelFileSizeNotAvailable: 'Dimensiunea nu este diponibilă', labelFileLoading: 'Se încarcă', labelFileLoadError: 'Eroare la încărcare', labelFileProcessing: 'Se încarcă', labelFileProcessingComplete: 'Încărcare finalizată', labelFileProcessingAborted: 'Încărcare anulată', labelFileProcessingError: 'Eroare la încărcare', labelFileProcessingRevertError: 'Eroare la anulare', labelFileRemoveError: 'Eroare la ştergere', labelTapToCancel: 'apasă pentru a anula', labelTapToRetry: 'apasă pentru a reîncerca', labelTapToUndo: 'apasă pentru a anula', labelButtonRemoveItem: 'Şterge', labelButtonAbortItemLoad: 'Anulează', labelButtonRetryItemLoad: 'Reîncearcă', labelButtonAbortItemProcessing: 'Anulează', labelButtonUndoItemProcessing: 'Anulează', labelButtonRetryItemProcessing: 'Reîncearcă', labelButtonProcessItem: 'Încarcă', labelMaxFileSizeExceeded: 'Fișierul este prea mare', labelMaxFileSize: 'Dimensiunea maximă a unui fișier este de {filesize}', labelMaxTotalFileSizeExceeded: 'Dimensiunea totală maximă a fost depășită', labelMaxTotalFileSize: 'Dimensiunea totală maximă a fișierelor este de {filesize}', labelFileTypeNotAllowed: 'Tipul fișierului nu este valid', fileValidateTypeLabelExpectedTypes: 'Se așteaptă {allButLastType} sau {lastType}', imageValidateSizeLabelFormatError: 'Formatul imaginii nu este acceptat', imageValidateSizeLabelImageSizeTooSmall: 'Imaginea este prea mică', imageValidateSizeLabelImageSizeTooBig: 'Imaginea este prea mare', imageValidateSizeLabelExpectedMinSize: 'Mărimea minimă este de {maxWidth} x {maxHeight}', imageValidateSizeLabelExpectedMaxSize: 'Mărimea maximă este de {maxWidth} x {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Rezoluția este prea mică', imageValidateSizeLabelImageResolutionTooHigh: 'Rezoluția este prea mare', imageValidateSizeLabelExpectedMinResolution: 'Rezoluția minimă este de {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Rezoluția maximă este de {maxResolution}' }; ================================================ FILE: locale/ru-ru.js ================================================ export default { labelIdle: 'Перетащите файлы или выберите ', labelInvalidField: 'Поле содержит недопустимые файлы', labelFileWaitingForSize: 'Укажите размер', labelFileSizeNotAvailable: 'Размер не поддерживается', labelFileLoading: 'Ожидание', labelFileLoadError: 'Ошибка при ожидании', labelFileProcessing: 'Загрузка', labelFileProcessingComplete: 'Загрузка завершена', labelFileProcessingAborted: 'Загрузка отменена', labelFileProcessingError: 'Ошибка при загрузке', labelFileProcessingRevertError: 'Ошибка при возврате', labelFileRemoveError: 'Ошибка при удалении', labelTapToCancel: 'нажмите для отмены', labelTapToRetry: 'нажмите, чтобы повторить попытку', labelTapToUndo: 'нажмите для отмены последнего действия', labelButtonRemoveItem: 'Удалить', labelButtonAbortItemLoad: 'Прекращено', labelButtonRetryItemLoad: 'Повторите попытку', labelButtonAbortItemProcessing: 'Отмена', labelButtonUndoItemProcessing: 'Отмена последнего действия', labelButtonRetryItemProcessing: 'Повторите попытку', labelButtonProcessItem: 'Загрузка', labelMaxFileSizeExceeded: 'Файл слишком большой', labelMaxFileSize: 'Максимальный размер файла: {filesize}', labelMaxTotalFileSizeExceeded: 'Превышен максимальный размер', labelMaxTotalFileSize: 'Максимальный размер файла: {filesize}', labelFileTypeNotAllowed: 'Файл неверного типа', fileValidateTypeLabelExpectedTypes: 'Ожидается {allButLastType} или {lastType}', imageValidateSizeLabelFormatError: 'Тип изображения не поддерживается', imageValidateSizeLabelImageSizeTooSmall: 'Изображение слишком маленькое', imageValidateSizeLabelImageSizeTooBig: 'Изображение слишком большое', imageValidateSizeLabelExpectedMinSize: 'Минимальный размер: {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Максимальный размер: {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Разрешение слишком низкое', imageValidateSizeLabelImageResolutionTooHigh: 'Разрешение слишком высокое', imageValidateSizeLabelExpectedMinResolution: 'Минимальное разрешение: {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Максимальное разрешение: {maxResolution}' }; ================================================ FILE: locale/sk-sk.js ================================================ export default { labelIdle: 'Natiahnúť súbor (drag&drop) alebo Vyhľadať ', labelInvalidField: 'Pole obsahuje chybné súbory', labelFileWaitingForSize: 'Zisťuje sa veľkosť', labelFileSizeNotAvailable: 'Neznáma veľkosť', labelFileLoading: 'Prenáša sa', labelFileLoadError: 'Chyba pri prenose', labelFileProcessing: 'Prebieha upload', labelFileProcessingComplete: 'Upload dokončený', labelFileProcessingAborted: 'Upload stornovaný', labelFileProcessingError: 'Chyba pri uploade', labelFileProcessingRevertError: 'Chyba pri obnove', labelFileRemoveError: 'Chyba pri odstránení', labelTapToCancel: 'Kliknite pre storno', labelTapToRetry: 'Kliknite pre opakovanie', labelTapToUndo: 'Kliknite pre vrátenie', labelButtonRemoveItem: 'Odstrániť', labelButtonAbortItemLoad: 'Storno', labelButtonRetryItemLoad: 'Opakovať', labelButtonAbortItemProcessing: 'Späť', labelButtonUndoItemProcessing: 'Vrátiť', labelButtonRetryItemProcessing: 'Opakovať', labelButtonProcessItem: 'Upload', labelMaxFileSizeExceeded: 'Súbor je príliš veľký', labelMaxFileSize: 'Najväčšia veľkosť súboru je {filesize}', labelMaxTotalFileSizeExceeded: 'Prekročená maximálna celková veľkosť súboru', labelMaxTotalFileSize: 'Maximálna celková veľkosť súboru je {filesize}', labelFileTypeNotAllowed: 'Súbor je nesprávneho typu', fileValidateTypeLabelExpectedTypes: 'Očakáva sa {allButLastType} alebo {lastType}', imageValidateSizeLabelFormatError: 'Obrázok tohto typu nie je podporovaný', imageValidateSizeLabelImageSizeTooSmall: 'Obrázok je príliš malý', imageValidateSizeLabelImageSizeTooBig: 'Obrázok je príliš veľký', imageValidateSizeLabelExpectedMinSize: 'Minimálny rozmer je {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maximálny rozmer je {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Rozlíšenie je príliš malé', imageValidateSizeLabelImageResolutionTooHigh: 'Rozlišenie je príliš veľké', imageValidateSizeLabelExpectedMinResolution: 'Minimálne rozlíšenie je {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maximálne rozlíšenie je {maxResolution}' }; ================================================ FILE: locale/sl-si.js ================================================ export default { labelIdle: 'Povleci in spusti ali izberi', labelInvalidField: 'Polje vsebuje neveljavne datoteke', labelFileWaitingForSize: 'Čakanje na velikost', labelFileSizeNotAvailable: 'Velikost ni na voljo', labelFileLoading: 'Nalaganje', labelFileLoadError: 'Napaka pri nalaganju', labelFileProcessing: 'Prenašanje', labelFileProcessingComplete: 'Prenos zaključen', labelFileProcessingAborted: 'Prenos preklican', labelFileProcessingError: 'Napaka pri prenosu', labelFileProcessingRevertError: 'Napaka pri razveljavitvi', labelFileRemoveError: 'Napaka pri odstranjevanju', labelTapToCancel: 'kliknite za preklic', labelTapToRetry: 'kliknite za ponovni poskus', labelTapToUndo: 'kliknite za razveljavitev', labelButtonRemoveItem: 'Odstrani', labelButtonAbortItemLoad: 'Prekini', labelButtonRetryItemLoad: 'Poskusi znova', labelButtonAbortItemProcessing: 'Prekliči', labelButtonUndoItemProcessing: 'Razveljavi', labelButtonRetryItemProcessing: 'Poskusi znova', labelButtonProcessItem: 'Prenesi', labelMaxFileSizeExceeded: 'Datoteka je prevelika', labelMaxFileSize: 'Največja velikost datoteke je {filesize}', labelMaxTotalFileSizeExceeded: 'Presežena je največja skupna velikost', labelMaxTotalFileSize: 'Največja skupna velikost datotek je {filesize}', labelFileTypeNotAllowed: 'Neveljavna vrsta datoteke', fileValidateTypeLabelExpectedTypes: 'Pričakuje se: {allButLastType} ali {lastType}', imageValidateSizeLabelFormatError: 'Vrsta slike ni podprta', imageValidateSizeLabelImageSizeTooSmall: 'Slika je premajhna', imageValidateSizeLabelImageSizeTooBig: 'Slika je prevelika', imageValidateSizeLabelExpectedMinSize: 'Najmanjša velikost je {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Največja velikost je {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Ločljivost je prenizka', imageValidateSizeLabelImageResolutionTooHigh: 'Ločljivost je previsoka', imageValidateSizeLabelExpectedMinResolution: 'Najmanjša ločljivost je {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Največja ločljivost je {maxResolution}' }; ================================================ FILE: locale/sr-rs.js ================================================ export default { labelIdle: 'Овде "испустите" датотеку или Претражи ', labelInvalidField: 'Поље садржи неисправне датотеке', labelFileWaitingForSize: 'Чекање на величину датотеке', labelFileSizeNotAvailable: 'Величина датотеке није доступна', labelFileLoading: 'Учитавање', labelFileLoadError: 'Грешка приликом учитавања', labelFileProcessing: 'Пренос', labelFileProcessingComplete: 'Пренос завршен', labelFileProcessingAborted: 'Пренос отказан', labelFileProcessingError: 'Грешка приликом преноса', labelFileProcessingRevertError: 'Грешка приликом враћања', labelFileRemoveError: 'Грешка приликом уклањања датотеке', labelTapToCancel: 'Додирни за прекид', labelTapToRetry: 'Додирни за поновно', labelTapToUndo: 'Додирни за враћање', labelButtonRemoveItem: 'Уклони', labelButtonAbortItemLoad: 'Одбаци', labelButtonRetryItemLoad: 'Понови', labelButtonAbortItemProcessing: 'Прекини', labelButtonUndoItemProcessing: 'Врати', labelButtonRetryItemProcessing: 'Понови', labelButtonProcessItem: 'Пренос', labelMaxFileSizeExceeded: 'Датотека је превелика', labelMaxFileSize: 'Максимална величина датотеке је {filesize}', labelMaxTotalFileSizeExceeded: 'Максимална укупна величина датотеке је прекорачена', labelMaxTotalFileSize: 'Максимална укупна величина датотеке је {filesize}', labelFileTypeNotAllowed: 'Тип датотеке није подржан', fileValidateTypeLabelExpectedTypes: 'Очекиван {allButLastType} или {lastType}', imageValidateSizeLabelFormatError: 'Тип слике није подржан', imageValidateSizeLabelImageSizeTooSmall: 'Слика је премала', imageValidateSizeLabelImageSizeTooBig: 'Слика је превелика', imageValidateSizeLabelExpectedMinSize: 'Минимална величина је {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Максимална величина је {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Резолуција је прениса', imageValidateSizeLabelImageResolutionTooHigh: 'Резолуција је превисока', imageValidateSizeLabelExpectedMinResolution: 'Минимална резолуција је {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Максимална резолуција је {maxResolution}' }; ================================================ FILE: locale/sv_se.js ================================================ export default { labelIdle: 'Drag och släpp dina filer eller Bläddra ', labelInvalidField: 'Fältet innehåller felaktiga filer', labelFileWaitingForSize: 'Väntar på storlek', labelFileSizeNotAvailable: 'Storleken finns inte tillgänglig', labelFileLoading: 'Laddar', labelFileLoadError: 'Fel under laddning', labelFileProcessing: 'Laddar upp', labelFileProcessingComplete: 'Uppladdning klar', labelFileProcessingAborted: 'Uppladdning avbruten', labelFileProcessingError: 'Fel under uppladdning', labelFileProcessingRevertError: 'Fel under återställning', labelFileRemoveError: 'Fel under borttagning', labelTapToCancel: 'tryck för att avbryta', labelTapToRetry: 'tryck för att försöka igen', labelTapToUndo: 'tryck för att ångra', labelButtonRemoveItem: 'Tabort', labelButtonAbortItemLoad: 'Avbryt', labelButtonRetryItemLoad: 'Försök igen', labelButtonAbortItemProcessing: 'Avbryt', labelButtonUndoItemProcessing: 'Ångra', labelButtonRetryItemProcessing: 'Försök igen', labelButtonProcessItem: 'Ladda upp', labelMaxFileSizeExceeded: 'Filen är för stor', labelMaxFileSize: 'Största tillåtna filstorlek är {filesize}', labelMaxTotalFileSizeExceeded: 'Maximal uppladdningsstorlek uppnåd', labelMaxTotalFileSize: 'Maximal uppladdningsstorlek är {filesize}', labelFileTypeNotAllowed: 'Felaktig filtyp', fileValidateTypeLabelExpectedTypes: 'Godkända filtyper {allButLastType} eller {lastType}', imageValidateSizeLabelFormatError: 'Bildtypen saknar stöd', imageValidateSizeLabelImageSizeTooSmall: 'Bilden är för liten', imageValidateSizeLabelImageSizeTooBig: 'Bilden är för stor', imageValidateSizeLabelExpectedMinSize: 'Minimal storlek är {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maximal storlek är {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Upplösningen är för låg', imageValidateSizeLabelImageResolutionTooHigh: 'Upplösningen är för hög', imageValidateSizeLabelExpectedMinResolution: 'Minsta tillåtna upplösning är {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Högsta tillåtna upplösning är {maxResolution}' }; ================================================ FILE: locale/tr-tr.js ================================================ export default { labelIdle: 'Dosyanızı Sürükleyin & Bırakın ya da Seçin ', labelInvalidField: 'Alan geçersiz dosyalar içeriyor', labelFileWaitingForSize: 'Boyut hesaplanıyor', labelFileSizeNotAvailable: 'Boyut mevcut değil', labelFileLoading: 'Yükleniyor', labelFileLoadError: 'Yükleme sırasında hata oluştu', labelFileProcessing: 'Yükleniyor', labelFileProcessingComplete: 'Yükleme tamamlandı', labelFileProcessingAborted: 'Yükleme iptal edildi', labelFileProcessingError: 'Yüklerken hata oluştu', labelFileProcessingRevertError: 'Geri çekerken hata oluştu', labelFileRemoveError: 'Kaldırırken hata oluştu', labelTapToCancel: 'İptal etmek için tıklayın', labelTapToRetry: 'Tekrar denemek için tıklayın', labelTapToUndo: 'Geri almak için tıklayın', labelButtonRemoveItem: 'Kaldır', labelButtonAbortItemLoad: 'İptal Et', labelButtonRetryItemLoad: 'Tekrar dene', labelButtonAbortItemProcessing: 'İptal et', labelButtonUndoItemProcessing: 'Geri Al', labelButtonRetryItemProcessing: 'Tekrar dene', labelButtonProcessItem: 'Yükle', labelMaxFileSizeExceeded: 'Dosya çok büyük', labelMaxFileSize: 'En fazla dosya boyutu: {filesize}', labelMaxTotalFileSizeExceeded: 'Maximum boyut aşıldı', labelMaxTotalFileSize: 'Maximum dosya boyutu :{filesize}', labelFileTypeNotAllowed: 'Geçersiz dosya tipi', fileValidateTypeLabelExpectedTypes: 'Şu {allButLastType} ya da şu dosya olması gerekir: {lastType}', imageValidateSizeLabelFormatError: 'Resim tipi desteklenmiyor', imageValidateSizeLabelImageSizeTooSmall: 'Resim çok küçük', imageValidateSizeLabelImageSizeTooBig: 'Resim çok büyük', imageValidateSizeLabelExpectedMinSize: 'Minimum boyut {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Maximum boyut {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Çözünürlük çok düşük', imageValidateSizeLabelImageResolutionTooHigh: 'Çözünürlük çok yüksek', imageValidateSizeLabelExpectedMinResolution: 'Minimum çözünürlük {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Maximum çözünürlük {maxResolution}' }; ================================================ FILE: locale/uk-ua.js ================================================ export default { labelIdle: 'Перетягніть файли або виберіть ', labelInvalidField: 'Поле містить недопустимі файли', labelFileWaitingForSize: 'Вкажіть розмір', labelFileSizeNotAvailable: 'Розмір не доступний', labelFileLoading: 'Очікування', labelFileLoadError: 'Помилка при очікуванні', labelFileProcessing: 'Завантаження', labelFileProcessingComplete: 'Завантаження завершено', labelFileProcessingAborted: 'Завантаження скасовано', labelFileProcessingError: 'Помилка при завантаженні', labelFileProcessingRevertError: 'Помилка при відновленні', labelFileRemoveError: 'Помилка при видаленні', labelTapToCancel: 'Відмінити', labelTapToRetry: 'Натисніть, щоб повторити спробу', labelTapToUndo: 'Натисніть, щоб відмінити останню дію', labelButtonRemoveItem: 'Видалити', labelButtonAbortItemLoad: 'Відмінити', labelButtonRetryItemLoad: 'Повторити спробу', labelButtonAbortItemProcessing: 'Відмінити', labelButtonUndoItemProcessing: 'Відмінити останню дію', labelButtonRetryItemProcessing: 'Повторити спробу', labelButtonProcessItem: 'Завантаження', labelMaxFileSizeExceeded: 'Файл занадто великий', labelMaxFileSize: 'Максимальний розмір файлу: {filesize}', labelMaxTotalFileSizeExceeded: 'Перевищено максимальний загальний розмір', labelMaxTotalFileSize: 'Максимальний загальний розмір: {filesize}', labelFileTypeNotAllowed: 'Формат файлу не підтримується', fileValidateTypeLabelExpectedTypes: 'Очікується {allButLastType} або {lastType}', imageValidateSizeLabelFormatError: 'Формат зображення не підтримується', imageValidateSizeLabelImageSizeTooSmall: 'Зображення занадто маленьке', imageValidateSizeLabelImageSizeTooBig: 'Зображення занадто велике', imageValidateSizeLabelExpectedMinSize: 'Мінімальний розмір: {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Максимальний розмір: {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Розміри зображення занадто маленькі', imageValidateSizeLabelImageResolutionTooHigh: 'Розміри зображення занадто великі', imageValidateSizeLabelExpectedMinResolution: 'Мінімальні розміри: {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Максимальні розміри: {maxResolution}' }; ================================================ FILE: locale/ur-ur.js ================================================ export default { labelIdle: 'اپنی فائلز ڈریگ کریں یا براؤز کریں ', labelInvalidField: 'فیلڈ میں غلط فائلز ہیں', labelFileWaitingForSize: 'سائز کا انتظار ہو رہا ہے', labelFileSizeNotAvailable: 'سائز دستیاب نہیں', labelFileLoading: 'لوڈ ہو رہی ہے', labelFileLoadError: 'لوڈنگ میں خرابی', labelFileProcessing: 'اپلوڈ ہو رہی ہے', labelFileProcessingComplete: 'اپلوڈ مکمل ہو گئی', labelFileProcessingAborted: 'اپلوڈ منسوخ کر دی گئی', labelFileProcessingError: 'اپلوڈ میں خرابی', labelFileProcessingRevertError: 'ریورٹ کرنے میں خرابی', labelFileRemoveError: 'فائل ہٹانے میں خرابی', labelTapToCancel: 'منسوخ کرنے کے لیے ٹیپ کریں', labelTapToRetry: 'دوبارہ کوشش کے لیے ٹیپ کریں', labelTapToUndo: 'واپس کرنے کے لیے ٹیپ کریں', labelButtonRemoveItem: 'ہٹائیں', labelButtonAbortItemLoad: 'روکیں', labelButtonRetryItemLoad: 'دوبارہ کوشش کریں', labelButtonAbortItemProcessing: 'منسوخ کریں', labelButtonUndoItemProcessing: 'واپس کریں', labelButtonRetryItemProcessing: 'دوبارہ کوشش کریں', labelButtonProcessItem: 'اپلوڈ کریں', labelMaxFileSizeExceeded: 'فائل بہت بڑی ہے', labelMaxFileSize: 'زیادہ سے زیادہ فائل سائز {filesize} ہے', labelMaxTotalFileSizeExceeded: 'مجموعی سائز کی حد تجاوز کر گئی ہے', labelMaxTotalFileSize: 'زیادہ سے زیادہ مجموعی فائل سائز {filesize} ہے', labelFileTypeNotAllowed: 'فائل کی قسم غیر درست ہے', fileValidateTypeLabelExpectedTypes: '{allButLastType} یا {lastType} متوقع ہیں', imageValidateSizeLabelFormatError: 'تصویر کی قسم سپورٹڈ نہیں', imageValidateSizeLabelImageSizeTooSmall: 'تصویر بہت چھوٹی ہے', imageValidateSizeLabelImageSizeTooBig: 'تصویر بہت بڑی ہے', imageValidateSizeLabelExpectedMinSize: 'کم از کم سائز {minWidth} × {minHeight} ہے', imageValidateSizeLabelExpectedMaxSize: 'زیادہ سے زیادہ سائز {maxWidth} × {maxHeight} ہے', imageValidateSizeLabelImageResolutionTooLow: 'ریزولوشن بہت کم ہے', imageValidateSizeLabelImageResolutionTooHigh: 'ریزولوشن بہت زیادہ ہے', imageValidateSizeLabelExpectedMinResolution: 'کم از کم ریزولوشن {minResolution} ہے', imageValidateSizeLabelExpectedMaxResolution: 'زیادہ سے زیادہ ریزولوشن {maxResolution} ہے' }; ================================================ FILE: locale/vi-vi.js ================================================ export default { labelIdle: 'Kéo thả tệp của bạn hoặc Tìm kiếm ', labelInvalidField: 'Trường chứa các tệp không hợp lệ', labelFileWaitingForSize: 'Đang chờ kích thước', labelFileSizeNotAvailable: 'Kích thước không có sẵn', labelFileLoading: 'Đang tải', labelFileLoadError: 'Lỗi khi tải', labelFileProcessing: 'Đang tải lên', labelFileProcessingComplete: 'Tải lên thành công', labelFileProcessingAborted: 'Đã huỷ tải lên', labelFileProcessingError: 'Lỗi khi tải lên', labelFileProcessingRevertError: 'Lỗi khi hoàn nguyên', labelFileRemoveError: 'Lỗi khi xóa', labelTapToCancel: 'nhấn để hủy', labelTapToRetry: 'nhấn để thử lại', labelTapToUndo: 'nhấn để hoàn tác', labelButtonRemoveItem: 'Xoá', labelButtonAbortItemLoad: 'Huỷ bỏ', labelButtonRetryItemLoad: 'Thử lại', labelButtonAbortItemProcessing: 'Hủy bỏ', labelButtonUndoItemProcessing: 'Hoàn tác', labelButtonRetryItemProcessing: 'Thử lại', labelButtonProcessItem: 'Tải lên', labelMaxFileSizeExceeded: 'Tập tin quá lớn', labelMaxFileSize: 'Kích thước tệp tối đa là {filesize}', labelMaxTotalFileSizeExceeded: 'Đã vượt quá tổng kích thước tối đa', labelMaxTotalFileSize: 'Tổng kích thước tệp tối đa là {filesize}', labelFileTypeNotAllowed: 'Tệp thuộc loại không hợp lệ', fileValidateTypeLabelExpectedTypes: 'Kiểu tệp hợp lệ là {allButLastType} hoặc {lastType}', imageValidateSizeLabelFormatError: 'Loại hình ảnh không được hỗ trợ', imageValidateSizeLabelImageSizeTooSmall: 'Hình ảnh quá nhỏ', imageValidateSizeLabelImageSizeTooBig: 'Hình ảnh quá lớn', imageValidateSizeLabelExpectedMinSize: 'Kích thước tối thiểu là {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: 'Kích thước tối đa là {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: 'Độ phân giải quá thấp', imageValidateSizeLabelImageResolutionTooHigh: 'Độ phân giải quá cao', imageValidateSizeLabelExpectedMinResolution: 'Độ phân giải tối thiểu là {minResolution}', imageValidateSizeLabelExpectedMaxResolution: 'Độ phân giải tối đa là {maxResolution}' }; ================================================ FILE: locale/zh-cn.js ================================================ export default { labelIdle: '拖放文件,或者 浏览 ', labelInvalidField: '字段包含无效文件', labelFileWaitingForSize: '计算文件大小', labelFileSizeNotAvailable: '文件大小不可用', labelFileLoading: '加载', labelFileLoadError: '加载错误', labelFileProcessing: '上传', labelFileProcessingComplete: '已上传', labelFileProcessingAborted: '上传已取消', labelFileProcessingError: '上传出错', labelFileProcessingRevertError: '还原出错', labelFileRemoveError: '删除出错', labelTapToCancel: '点击取消', labelTapToRetry: '点击重试', labelTapToUndo: '点击撤消', labelButtonRemoveItem: '删除', labelButtonAbortItemLoad: '中止', labelButtonRetryItemLoad: '重试', labelButtonAbortItemProcessing: '取消', labelButtonUndoItemProcessing: '撤消', labelButtonRetryItemProcessing: '重试', labelButtonProcessItem: '上传', labelMaxFileSizeExceeded: '文件太大', labelMaxFileSize: '最大值: {filesize}', labelMaxTotalFileSizeExceeded: '超过最大文件大小', labelMaxTotalFileSize: '最大文件大小:{filesize}', labelFileTypeNotAllowed: '文件类型无效', fileValidateTypeLabelExpectedTypes: '应为 {allButLastType} 或 {lastType}', imageValidateSizeLabelFormatError: '不支持图像类型', imageValidateSizeLabelImageSizeTooSmall: '图像太小', imageValidateSizeLabelImageSizeTooBig: '图像太大', imageValidateSizeLabelExpectedMinSize: '最小值: {minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: '最大值: {maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: '分辨率太低', imageValidateSizeLabelImageResolutionTooHigh: '分辨率太高', imageValidateSizeLabelExpectedMinResolution: '最小分辨率:{minResolution}', imageValidateSizeLabelExpectedMaxResolution: '最大分辨率:{maxResolution}' }; ================================================ FILE: locale/zh-hk.js ================================================ export default { labelIdle: '拖放檔案,或者 瀏覽 ', labelInvalidField: '不支援此檔案', labelFileWaitingForSize: '正在計算檔案大小', labelFileSizeNotAvailable: '檔案大小不符', labelFileLoading: '讀取中', labelFileLoadError: '讀取錯誤', labelFileProcessing: '上傳', labelFileProcessingComplete: '已上傳', labelFileProcessingAborted: '上傳已取消', labelFileProcessingError: '上傳發生錯誤', labelFileProcessingRevertError: '還原錯誤', labelFileRemoveError: '刪除錯誤', labelTapToCancel: '點擊取消', labelTapToRetry: '點擊重試', labelTapToUndo: '點擊還原', labelButtonRemoveItem: '刪除', labelButtonAbortItemLoad: '停止', labelButtonRetryItemLoad: '重試', labelButtonAbortItemProcessing: '取消', labelButtonUndoItemProcessing: '取消', labelButtonRetryItemProcessing: '重試', labelButtonProcessItem: '上傳', labelMaxFileSizeExceeded: '檔案過大', labelMaxFileSize: '最大值:{filesize}', labelMaxTotalFileSizeExceeded: '超過最大可上傳大小', labelMaxTotalFileSize: '最大可上傳大小:{filesize}', labelFileTypeNotAllowed: '不支援此類型檔案', fileValidateTypeLabelExpectedTypes: '應為 {allButLastType} 或 {lastType}', imageValidateSizeLabelFormatError: '不支持此類圖片類型', imageValidateSizeLabelImageSizeTooSmall: '圖片過小', imageValidateSizeLabelImageSizeTooBig: '圖片過大', imageValidateSizeLabelExpectedMinSize: '最小尺寸:{minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: '最大尺寸:{maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: '解析度過低', imageValidateSizeLabelImageResolutionTooHigh: '解析度過高', imageValidateSizeLabelExpectedMinResolution: '最低解析度:{minResolution}', imageValidateSizeLabelExpectedMaxResolution: '最高解析度:{maxResolution}' }; ================================================ FILE: locale/zh-tw.js ================================================ export default { labelIdle: '拖放檔案,或者 瀏覽 ', labelInvalidField: '不支援此檔案', labelFileWaitingForSize: '正在計算檔案大小', labelFileSizeNotAvailable: '檔案大小不符', labelFileLoading: '讀取中', labelFileLoadError: '讀取錯誤', labelFileProcessing: '上傳', labelFileProcessingComplete: '已上傳', labelFileProcessingAborted: '上傳已取消', labelFileProcessingError: '上傳發生錯誤', labelFileProcessingRevertError: '還原錯誤', labelFileRemoveError: '刪除錯誤', labelTapToCancel: '點擊取消', labelTapToRetry: '點擊重試', labelTapToUndo: '點擊還原', labelButtonRemoveItem: '刪除', labelButtonAbortItemLoad: '停止', labelButtonRetryItemLoad: '重試', labelButtonAbortItemProcessing: '取消', labelButtonUndoItemProcessing: '取消', labelButtonRetryItemProcessing: '重試', labelButtonProcessItem: '上傳', labelMaxFileSizeExceeded: '檔案過大', labelMaxFileSize: '最大值:{filesize}', labelMaxTotalFileSizeExceeded: '超過最大可上傳大小', labelMaxTotalFileSize: '最大可上傳大小:{filesize}', labelFileTypeNotAllowed: '不支援此類型檔案', fileValidateTypeLabelExpectedTypes: '應為 {allButLastType} 或 {lastType}', imageValidateSizeLabelFormatError: '不支持此類圖片類型', imageValidateSizeLabelImageSizeTooSmall: '圖片過小', imageValidateSizeLabelImageSizeTooBig: '圖片過大', imageValidateSizeLabelExpectedMinSize: '最小尺寸:{minWidth} × {minHeight}', imageValidateSizeLabelExpectedMaxSize: '最大尺寸:{maxWidth} × {maxHeight}', imageValidateSizeLabelImageResolutionTooLow: '解析度過低', imageValidateSizeLabelImageResolutionTooHigh: '解析度過高', imageValidateSizeLabelExpectedMinResolution: '最低解析度:{minResolution}', imageValidateSizeLabelExpectedMaxResolution: '最高解析度:{maxResolution}' }; ================================================ FILE: package.json ================================================ { "name": "filepond", "version": "4.32.12", "description": "FilePond, Where files go to stretch their bits.", "license": "MIT", "author": { "name": "PQINA", "url": "https://pqina.nl/" }, "homepage": "https://pqina.nl/filepond/", "repository": "pqina/filepond", "main": "dist/filepond.js", "browser": "dist/filepond.js", "module": "dist/filepond.esm.js", "keywords": [ "javascript", "file", "upload", "drag", "drop", "browse", "paste", "image", "preview" ], "browserslist": [ "last 1 version and not Explorer 10", "Explorer 11", "iOS >= 9", "Android >= 4.4" ], "files": [ "dist", "locale", "types/*.d.ts" ], "types": "types/index.d.ts", "scripts": { "test": "npx jest", "dev": "npm run start", "start": "npx rollup -c -w", "build": "npm run scripts | npm run styles", "scripts": "npx rollup -c", "styles": "npm run styles:pretty && npm run styles:nano", "styles:pretty": "npx postcss src/css/styles.css --no-map --use precss --use autoprefixer | npx prettier --single-quote --parser css | node banner-cli.js FilePond > dist/filepond.css", "styles:nano": "npx postcss src/css/styles.css --no-map --use precss --use autoprefixer --use cssnano | node banner-cli.js FilePond > dist/filepond.min.css", "dtslint": "dtslint types" }, "devDependencies": { "@babel/core": "^7.5.5", "@babel/plugin-proposal-object-rest-spread": "^7.5.5", "@babel/plugin-transform-template-literals": "^7.4.4", "@babel/preset-env": "^7.5.5", "autoprefixer": "^9.6.1", "babel-jest": "^24.8.0", "cssnano": "^4.1.10", "dtslint": "^3.6.12", "jest": "^24.8.0", "jest-mock-console": "^1.2.3", "postcss-cli": "^6.1.3", "precss": "^4.0.0", "prettier": "^1.18.2", "rollup": "^1.17.0", "rollup-plugin-babel": "^4.3.3", "rollup-plugin-commonjs": "^9.3.4", "rollup-plugin-license": "^0.8.1", "rollup-plugin-node-resolve": "^4.2.4", "rollup-plugin-prettier": "^0.6.0", "rollup-plugin-terser": "^4.0.4", "typescript": "^3.9.6" } } ================================================ FILE: rollup.config.js ================================================ import * as pkg from './package.json'; import build from './rollup.scripts'; export default build( { id: 'FilePond', ...pkg }, [ { format: 'umd', transpile: true }, { format: 'umd', transpile: true, minify: true }, { format: 'es' }, { format: 'es', minify: true } ] ); ================================================ FILE: rollup.scripts.js ================================================ import babel from 'rollup-plugin-babel'; import license from 'rollup-plugin-license'; import { terser } from 'rollup-plugin-terser'; import prettier from 'rollup-plugin-prettier'; const banner = require('./banner'); const createBuild = (options) => { const { format, id, name, minify = false, transpile = false } = options; // get filename const filename = ['dist/', name]; if (format === 'es') { filename.push('.esm'); } if (minify) { filename.push('.min'); } filename.push('.js'); // collect plugins const plugins = []; if (transpile) { plugins.push(babel({ exclude: ['node_modules/**'] })); } if (minify) { plugins.push(terser()); } else { plugins.push(prettier({ singleQuote: true, parser: 'babel' })); } plugins.push(license({banner: banner(options)})); // return Rollup config return { input: 'src/js/index.js', treeshake: false, output: [ { format, name: id, file: filename.join('') } ], plugins } }; export default (metadata, configs) => configs.map(config => createBuild({ ...metadata, ...config })); ================================================ FILE: src/css/assistant.css ================================================ .filepond--assistant { position: absolute; overflow: hidden; height: 1px; width: 1px; padding: 0; border: 0; clip: rect(1px, 1px, 1px, 1px); clip-path: inset(50%); white-space: nowrap; } ================================================ FILE: src/css/browser.css ================================================ /* Hard to override styles */ .filepond--browser.filepond--browser { /* is positioned absolute so it is focusable for form validation errors */ position: absolute; margin: 0; padding: 0; /* is positioned ~behind drop label */ left: 1em; top: 1.75em; width: calc(100% - 2em); /* hide visually */ opacity: 0; font-size: 0; /* removes text cursor in Internet Explorer 11 */ } ================================================ FILE: src/css/data.css ================================================ .filepond--data { position: absolute; width: 0; height: 0; padding: 0; margin: 0; border: none; visibility: hidden; pointer-events: none; contain: strict; } ================================================ FILE: src/css/drip.css ================================================ .filepond--drip { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; opacity: 0.1; /* can't interact with this element */ pointer-events: none; /* inherit border radius from parent (needed for drip-blob cut of) */ border-radius: 0.5em; /* this seems to prevent Chrome from redrawing this layer constantly */ background: rgba(0, 0, 0, 0.01); } .filepond--drip-blob { position: absolute; transform-origin: center center; top: 0; left: 0; width: 8em; height: 8em; margin-left: -4em; margin-top: -4em; background: #292625; border-radius: 50%; /* will be animated */ will-change: transform, opacity; } ================================================ FILE: src/css/drop-label.css ================================================ .filepond--drop-label { position: absolute; left: 0; right: 0; top: 0; margin: 0; color: #4f4f4f; /* center contents */ display: flex; justify-content: center; align-items: center; /* fixes IE11 centering problems (is overruled by label min-height) */ height: 0px; /* dont allow selection */ user-select: none; /* will be animated */ will-change: transform, opacity; } /* Hard to override styles on purpose */ .filepond--drop-label.filepond--drop-label label { display: block; margin: 0; padding: 0.5em; /* use padding instead of margin so click area is not impacted */ } .filepond--drop-label label { cursor: default; font-size: 0.875em; font-weight: normal; text-align: center; line-height: 1.5; } .filepond--label-action { text-decoration: underline; text-decoration-skip-ink: auto; text-decoration-color: #a7a4a4; cursor: pointer; } .filepond--root[data-disabled] { & .filepond--drop-label label { opacity: 0.5; } } ================================================ FILE: src/css/file-action-button.css ================================================ /* Hard to override styles */ .filepond--file-action-button.filepond--file-action-button { font-size: 1em; width: 1.625em; height: 1.625em; font-family: inherit; line-height: inherit; margin: 0; padding: 0; border: none; outline: none; will-change: transform, opacity; /* hidden label */ & span { position: absolute; overflow: hidden; height: 1px; width: 1px; padding: 0; border: 0; clip: rect(1px, 1px, 1px, 1px); clip-path: inset(50%); white-space: nowrap; } /* scale SVG to fill button */ & svg { width: 100%; height: 100%; } /* bigger touch area */ &::after { position: absolute; left: -0.75em; right: -0.75em; top: -0.75em; bottom: -0.75em; content: ''; } } /* Soft styles */ .filepond--file-action-button { /* use default arrow cursor */ cursor: auto; /* reset default button styles */ color: #fff; /* set default look n feel */ border-radius: 50%; background-color: rgba(0, 0, 0, 0.5); background-image: none; /* we animate box shadow on focus */ /* it's only slightly slower than animating */ /* a pseudo-element with transforms and renders */ /* a lot better on chrome */ box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); transition: box-shadow 0.25s ease-in; &:hover, &:focus { box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.9); } &[disabled] { color: rgba(255, 255, 255, 0.5); background-color: rgba(0, 0, 0, 0.25); } &[hidden] { display: none; } } /* edit button */ .filepond--action-edit-item.filepond--action-edit-item { width: 2em; height: 2em; padding: 0.1875em; &[data-align*='center'] { margin-left: -0.1875em; } &[data-align*='bottom'] { margin-bottom: -0.1875em; } } .filepond--action-edit-item-alt { border: none; line-height: inherit; background: transparent; font-family: inherit; color: inherit; outline: none; padding: 0; margin: 0 0 0 0.25em; pointer-events: all; position: absolute; svg { width: 1.3125em; height: 1.3125em; } span { font-size: 0; opacity: 0; } } ================================================ FILE: src/css/file-info.css ================================================ .filepond--file-info { position: static; display: flex; flex-direction: column; align-items: flex-start; flex: 1; margin: 0 0.5em 0 0; min-width: 0; /* will be animated */ will-change: transform, opacity; /* can't do anything with this info */ pointer-events: none; user-select: none; /* no margins on children */ & * { margin: 0; } /* we don't want to have these overrules so these selectors are a bit more specific */ & .filepond--file-info-main { font-size: 0.75em; line-height: 1.2; /* we want ellipsis if this bar gets too wide */ text-overflow: ellipsis; overflow: hidden; white-space: nowrap; width: 100%; } & .filepond--file-info-sub { font-size: 0.625em; opacity: 0.5; transition: opacity 0.25s ease-in-out; white-space: nowrap; } & .filepond--file-info-sub:empty { display: none; } } ================================================ FILE: src/css/file-status.css ================================================ .filepond--file-status { position: static; display: flex; flex-direction: column; align-items: flex-end; flex-grow: 0; flex-shrink: 0; margin: 0; min-width: 2.25em; text-align: right; /* will be animated */ will-change: transform, opacity; /* can't do anything with this info */ pointer-events: none; user-select: none; /* no margins on children */ & * { margin: 0; white-space: nowrap; } /* font sizes */ & .filepond--file-status-main { font-size: 0.75em; line-height: 1.2; } & .filepond--file-status-sub { font-size: 0.625em; opacity: 0.5; transition: opacity 0.25s ease-in-out; } } ================================================ FILE: src/css/file-wrapper.css ================================================ /* Hard to override styles */ .filepond--file-wrapper.filepond--file-wrapper { border: none; margin: 0; padding: 0; min-width: 0; height: 100%; /* hide legend for visual users */ & > legend { position: absolute; overflow: hidden; height: 1px; width: 1px; padding: 0; border: 0; clip: rect(1px, 1px, 1px, 1px); clip-path: inset(50%); white-space: nowrap; } } ================================================ FILE: src/css/file.css ================================================ .filepond--file { position: static; display: flex; height: 100%; align-items: flex-start; padding: 0.5625em 0.5625em; color: #fff; border-radius: 0.5em; /* control positions */ & .filepond--file-status { margin-left: auto; margin-right: 2.25em; } & .filepond--processing-complete-indicator { pointer-events: none; user-select: none; z-index: 3; } & .filepond--processing-complete-indicator, & .filepond--progress-indicator, & .filepond--file-action-button { position: absolute; } /* .filepond--file-action-button */ & [data-align*='left'] { left: 0.5625em; } & [data-align*='right'] { right: 0.5625em; } & [data-align*='center'] { left: calc(50% - 0.8125em); /* .8125 is half of button width */ } & [data-align*='bottom'] { bottom: 1.125em; } & [data-align='center'] { top: calc(50% - 0.8125em); } & .filepond--progress-indicator { margin-top: 0.1875em; &[data-align*='right'] { margin-right: 0.1875em; } &[data-align*='left'] { margin-left: 0.1875em; } } } /* make sure text does not overlap */ [data-filepond-item-state='cancelled'], [data-filepond-item-state*='invalid'], [data-filepond-item-state*='error'] { & .filepond--file-info { margin-right: 2.25em; } } [data-filepond-item-state~='processing'] { & .filepond--file-status-sub { opacity: 0; } & .filepond--action-abort-item-processing ~ .filepond--file-status .filepond--file-status-sub { opacity: 0.5; } } [data-filepond-item-state='processing-error'] { & .filepond--file-status-sub { opacity: 0; } & .filepond--action-retry-item-processing ~ .filepond--file-status .filepond--file-status-sub { opacity: 0.5; } } [data-filepond-item-state='processing-complete'] { /* busy state */ & .filepond--action-revert-item-processing svg { animation: fall 0.5s 0.125s linear both; } /* hide details by default, only show when can revert */ & .filepond--file-status-sub { opacity: 0.5; } & .filepond--processing-complete-indicator:not([style*='hidden']) ~ .filepond--file-status .filepond--file-status-sub { opacity: 0; } & .filepond--file-info-sub { opacity: 0; } & .filepond--action-revert-item-processing ~ .filepond--file-info .filepond--file-info-sub { opacity: 0.5; } } /* file state can be invalid or error, both are visually similar but */ /* having them as separate states might be useful */ [data-filepond-item-state*='invalid'], [data-filepond-item-state*='error'] { & .filepond--panel, & .filepond--file-wrapper { animation: shake 0.65s linear both; } } /* spins progress indicator when file is marked as busy */ [data-filepond-item-state*='busy'] { & .filepond--progress-indicator svg { animation: spin 1s linear infinite; } } /** * States */ @keyframes spin { 0% { transform: rotateZ(0deg); } 100% { transform: rotateZ(360deg); } } @keyframes shake { 10%, 90% { transform: translateX(-0.0625em); } 20%, 80% { transform: translateX(0.125em); } 30%, 50%, 70% { transform: translateX(-0.25em); } 40%, 60% { transform: translateX(0.25em); } } @keyframes fall { 0% { opacity: 0; transform: scale(0.5); animation-timing-function: ease-out; } 70% { opacity: 1; transform: scale(1.1); animation-timing-function: ease-in-out; } 100% { transform: scale(1); animation-timing-function: ease-out; } } ================================================ FILE: src/css/hopper.css ================================================ /* ignore all other interaction elements while dragging a file */ .filepond--hopper[data-hopper-state='drag-over'] > * { pointer-events: none; } /* capture all hit tests using a hidden layer, this speeds up the event flow */ .filepond--hopper[data-hopper-state='drag-over']::after { content: ''; position: absolute; left: 0; top: 0; right: 0; bottom: 0; z-index: 100; } ================================================ FILE: src/css/item-order.css ================================================ .filepond--progress-indicator { z-index: 103; } .filepond--file-action-button { z-index: 102; } .filepond--file-status { z-index: 101; } .filepond--file-info { z-index: 100; } ================================================ FILE: src/css/item.css ================================================ .filepond--item { position: absolute; top: 0; left: 0; right: 0; z-index: 1; padding: 0; margin: 0.25em; will-change: transform, opacity; touch-action: auto; /* item children order */ & > .filepond--panel { z-index: -1; /* has a slight shadow */ .filepond--panel-bottom { box-shadow: 0 0.0625em 0.125em -0.0625em rgba(0, 0, 0, 0.25); } } /* drag related */ & > .filepond--file-wrapper, & > .filepond--panel { transition: opacity 0.15s ease-out; } &[data-drag-state] { cursor: grab; > .filepond--panel { transition: box-shadow 0.125s ease-in-out; box-shadow: 0 0 0 rgba(0, 0, 0, 0); } } &[data-drag-state='drag'] { cursor: grabbing; > .filepond--panel { box-shadow: 0 0.125em 0.3125em rgba(0, 0, 0, 0.325); } } &[data-drag-state]:not([data-drag-state='idle']) { z-index: 2; } } /* states */ .filepond--item-panel { background-color: #64605e; } [data-filepond-item-state='processing-complete'] { .filepond--item-panel { background-color: #369763; } } [data-filepond-item-state*='invalid'], [data-filepond-item-state*='error'] { .filepond--item-panel { background-color: #c44e47; } } /* style of item panel */ .filepond--item-panel { border-radius: 0.5em; transition: background-color 0.25s; } ================================================ FILE: src/css/list-scroller.css ================================================ /* normal mode */ .filepond--list-scroller { position: absolute; top: 0; left: 0; right: 0; margin: 0; will-change: transform; } /* scroll mode */ .filepond--list-scroller[data-state='overflow'] { & .filepond--list { bottom: 0; right: 0; } overflow-y: scroll; overflow-x: hidden; -webkit-overflow-scrolling: touch; mask: linear-gradient(to bottom, #000 calc(100% - 0.5em), transparent 100%); } /* style scrollbar */ .filepond--list-scroller::-webkit-scrollbar { background: transparent; } .filepond--list-scroller::-webkit-scrollbar:vertical { width: 1em; } .filepond--list-scroller::-webkit-scrollbar:horizontal { height: 0; } .filepond--list-scroller::-webkit-scrollbar-thumb { background-color: rgba(0, 0, 0, 0.3); border-radius: 99999px; border: 0.3125em solid transparent; background-clip: content-box; } ================================================ FILE: src/css/list.css ================================================ /* hard to overide styles on purpose */ .filepond--list.filepond--list { position: absolute; top: 0; margin: 0; padding: 0; list-style-type: none; /* prevents endless paint calls on filepond--list-scroller */ will-change: transform; } /* used for padding so allowed to be restyled */ .filepond--list { left: 0.75em; right: 0.75em; } ================================================ FILE: src/css/modifiers.css ================================================ .filepond--root { &[data-style-panel-layout~='integrated'] { width: 100%; height: 100%; max-width: none; margin: 0; } &[data-style-panel-layout~='circle'], &[data-style-panel-layout~='integrated'] { & .filepond--panel-root { border-radius: 0; > * { display: none; } } & .filepond--drop-label { bottom: 0; height: auto; display: flex; justify-content: center; align-items: center; z-index: 7; } /* we're only loading one item, this makes the intro animation a bit nicer */ & .filepond--item-panel { display: none; } } &[data-style-panel-layout~='compact'], &[data-style-panel-layout~='integrated'] { & .filepond--list-scroller { overflow: hidden; height: 100%; margin-top: 0; margin-bottom: 0; } & .filepond--list { left: 0; right: 0; height: 100%; } & .filepond--item { margin: 0; } & .filepond--file-wrapper { height: 100%; } & .filepond--drop-label { z-index: 7; } } &[data-style-panel-layout~='circle'] { border-radius: 99999rem; overflow: hidden; & > .filepond--panel { border-radius: inherit; > * { display: none; } } /* circle cuts of this info, so best to hide it */ & .filepond--file-info { display: none; } & .filepond--file-status { display: none; } & .filepond--action-edit-item { opacity: 1 !important; visibility: visible !important; } } } /* dirfty way to fix circular overflow issue on safari 11+ */ @media not all and (min-resolution: 0.001dpcm) { @supports (-webkit-appearance: none) and (stroke-color: transparent) { .filepond--root[data-style-panel-layout~='circle'] { will-change: transform; } } } ================================================ FILE: src/css/panel-root.css ================================================ .filepond--panel-root { border-radius: 0.5em; background-color: #f1f0ef; } ================================================ FILE: src/css/panel.css ================================================ .filepond--panel { position: absolute; left: 0; top: 0; right: 0; margin: 0; /* defaults to 100% height (fixed height mode) this fixes problem with panel height in IE11 */ height: 100% !important; /* no interaction possible with panel */ pointer-events: none; } .filepond-panel:not([data-scalable='false']) { height: auto !important; } .filepond--panel[data-scalable='false'] { > div { display: none; } } .filepond--panel[data-scalable='true'] { /* this seems to fix Chrome performance issues */ /* - when box-shadow is enabled */ /* - when multiple ponds are active on the same page */ transform-style: preserve-3d; /* prevent borders and backgrounds */ background-color: transparent !important; border: none !important; } .filepond--panel-top, .filepond--panel-bottom, .filepond--panel-center { position: absolute; left: 0; top: 0; right: 0; margin: 0; padding: 0; } .filepond--panel-top, .filepond--panel-bottom { height: 0.5em; } .filepond--panel-top { border-bottom-left-radius: 0 !important; border-bottom-right-radius: 0 !important; border-bottom: none !important; /* fixes tiny transparant line between top and center panel */ &::after { content: ''; position: absolute; height: 2px; left: 0; right: 0; bottom: -1px; background-color: inherit; } } .filepond--panel-center, .filepond--panel-bottom { will-change: transform; backface-visibility: hidden; transform-origin: left top; transform: translate3d(0, 0.5em, 0); } .filepond--panel-bottom { border-top-left-radius: 0 !important; border-top-right-radius: 0 !important; border-top: none !important; /* fixes tiny transparant line between bottom and center of panel */ &::before { content: ''; position: absolute; height: 2px; left: 0; right: 0; top: -1px; background-color: inherit; } } .filepond--panel-center { /* the center panel is scaled using scale3d to fit the correct height */ /* we use 100px instead of 1px as scaling 1px to a huge height is really laggy on chrome */ height: 100px !important; border-top: none !important; border-bottom: none !important; border-radius: 0 !important; /* hide if not transformed, prevents a little flash when the panel is at 100px height while attached for first time */ &:not([style]) { visibility: hidden; } } ================================================ FILE: src/css/progress-indicator.css ================================================ .filepond--progress-indicator { position: static; width: 1.25em; height: 1.25em; color: #fff; /* can't have margins */ margin: 0; /* no interaction possible with progress indicator */ pointer-events: none; /* will be animated */ will-change: transform, opacity; } .filepond--progress-indicator svg { width: 100%; height: 100%; vertical-align: top; transform-box: fill-box; /* should center the animation correctly when zoomed in */ } .filepond--progress-indicator path { fill: none; stroke: currentColor; } ================================================ FILE: src/css/root-order.css ================================================ .filepond--list-scroller { z-index: 6; } .filepond--drop-label { z-index: 5; } .filepond--drip { z-index: 3; } .filepond--root > .filepond--panel { z-index: 2; } .filepond--browser { z-index: 1; } ================================================ FILE: src/css/root.css ================================================ .filepond--root { /* layout*/ box-sizing: border-box; position: relative; margin-bottom: 1em; /* base font size for whole component */ font-size: 1rem; /* base line height */ line-height: normal; /* up uses default system font family */ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; /* will increase font weight a bit on Safari */ font-weight: 450; /* default text alignment */ text-align: left; /* better text rendering on Safari */ text-rendering: optimizeLegibility; /* text direction is ltr for now */ direction: ltr; /* optimize rendering */ /* https://developer.mozilla.org/en-US/docs/Web/CSS/contain */ contain: layout style size; /* correct box sizing, line-height and positioning on child elements */ & * { box-sizing: inherit; line-height: inherit; } & *:not(text) { font-size: inherit; } /* block everything */ &[data-disabled] { pointer-events: none; .filepond--list-scroller { pointer-events: all; } .filepond--list { pointer-events: none; } } } /** * Root element children layout */ .filepond--root { & .filepond--drop-label { min-height: 4.75em; } & .filepond--list-scroller { margin-top: 1em; margin-bottom: 1em; } & .filepond--credits { position: absolute; right: 0; opacity: 0.4; line-height: 0.85; font-size: 11px; color: inherit; text-decoration: none; z-index: 3; bottom: -14px; } & .filepond--credits[style] { top: 0; bottom: auto; margin-top: 14px; } } ================================================ FILE: src/css/styles.css ================================================ @import 'assistant.css'; @import 'browser.css'; @import 'data.css'; @import 'drip.css'; @import 'drop-label.css'; @import 'file-action-button.css'; @import 'file-info.css'; @import 'file-status.css'; @import 'file-wrapper.css'; @import 'file.css'; @import 'hopper.css'; @import 'item-order.css'; @import 'item.css'; @import 'list-scroller.css'; @import 'list.css'; @import 'modifiers.css'; @import 'panel-root.css'; @import 'panel.css'; @import 'progress-indicator.css'; @import 'root-order.css'; @import 'root.css'; ================================================ FILE: src/js/__tests__/addFile.test.js ================================================ import './windowMatchMedia.mock'; import { create } from '../index.js'; describe('adding files', () => { let pond = null; beforeEach(() => { if (pond) { pond.destroy(); } pond = create(); // enables draw loop, else it seems that filepond is hidden Object.defineProperty(pond.element, 'offsetParent', { get: jest.fn(() => 1), }); }); test('add file', done => { const data = new File(['Hello World!'], 'dummy.txt', { type: 'text/plain', lastModified: new Date(), }); pond.addFile(data).then(item => { done(); }); }); test('add blob', done => { const data = new Blob(['Hello World!'], { type: 'text/plain' }); pond.addFile(data).then(item => { done(); }); }); test('add base64 string', done => { const data = 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=='; pond.addFile(data).then(item => { done(); }); }); }); ================================================ FILE: src/js/__tests__/callbacks.test.js ================================================ import mockConsole from "jest-mock-console"; import './windowMatchMedia.mock'; import { create, OptionTypes, FileStatus } from '../index.js'; describe('adding files', () => { const data = 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=='; let pond = null; const createPond = options => { if (pond) { pond.destroy(); } pond = create({ server: { process: (fieldName, file, metadata, load, error, progress, abort) => { let p = 0; const interval = setInterval(() => { p += 0.01; progress(true, p, 1); }, 50); const timeout = setTimeout(() => { clearInterval(interval); progress(true, 1, 1); load(Date.now()); }, 750); return { abort: () => { clearTimeout(timeout); abort(); }, }; }, }, ...options, }); // enables draw loop, else it seems that filepond is hidden and it won't run Object.defineProperty(pond.element, 'offsetParent', { get: jest.fn(() => 1), }); }; test('oninit', done => { createPond({ oninit: () => { done(); }, }); }); test('onaddfilestart', done => { createPond(); pond.onaddfilestart = () => { done(); }; pond.addFile(data); }); test('onaddfileprogress', done => { createPond(); pond.onaddfilestart = () => { done(); }; pond.addFile(data); }); test('onaddfile', done => { createPond(); pond.onaddfilestart = () => { done(); }; pond.addFile(data); }); test('onerror', done => { // we don't want the console error about the server call failure to muddy up the console const restoreConsole = mockConsole(); createPond({ server: './invalid-path' }); pond.onerror = () => { done(); }; pond.addFile(data); // restore the console back to normal so that real issues aren't hidden restoreConsole(); }); test('onremovefile', done => { createPond(); pond.files = [data]; pond.onremovefile = () => { done(); }; pond.removeFile(); }); test('onprocessfilestart', done => { createPond(); pond.onprocessfilestart = () => { done(); }; pond.files = [data]; }); test('onprocessfileprogress', done => { createPond(); pond.onprocessfileprogress = () => { done(); }; pond.files = [data]; }); test('onprocessfileabort', done => { createPond(); pond.onprocessfileabort = () => { done(); }; pond.files = [data]; pond.getFile().abortProcessing(); }); test('onprocessfile', done => { createPond(); pond.onprocessfile = () => { done(); }; pond.files = [data]; }); test('onprocessfiles', done => { createPond(); pond.onprocessfiles = () => { const result = pond .getFiles() .every(file => file.status === FileStatus.PROCESSING_COMPLETE); expect(result).toBe(true); done(); }; pond.files = [data, data]; }); }); ================================================ FILE: src/js/__tests__/contentDisposition.test.js ================================================ import { getFileNameFromHeader } from '../utils/getFileInfoFromHeaders'; describe('parse filename', () => { [ { value: ``, expected: null }, { value: `foo-bar`, expected: null }, { value: `Content-Disposition: attachment;`, expected: null }, { value: `Content-Disposition: attachment; filename=`, expected: null }, { value: `Content-Disposition: attachment; filename.jpg`, expected: null }, { value: `Content-Disposition: attachment; filename=filename.jpg`, expected: 'filename.jpg', }, { value: `Content-Disposition: attachment; filename=filename.jpg;`, expected: 'filename.jpg', }, { value: `Content-Disposition: attachment; filename="filename.jpg"`, expected: 'filename.jpg', }, { value: `Content-Disposition: attachment; filename="filename.jpg";`, expected: 'filename.jpg', }, { value: `Content-Disposition: attachment; filename=file name.jpg`, expected: 'file name.jpg', }, { value: `Content-Disposition: attachment; filename=filename.jpg; filename*=UTF-8''filename.jpg`, expected: 'filename.jpg', }, { value: `Content-Disposition: attachment; filename="filename.jpg"; filename*=UTF-8''filename.jpg`, expected: 'filename.jpg', }, { value: `Content-Disposition: attachment; filename="file name.jpg"; filename*=UTF-8''file%20name.jpg`, expected: 'file name.jpg', }, ].forEach(header => { test(`Can parse: "${header.value}"`, () => { const name = getFileNameFromHeader(header.value); expect(name).toBe(header.expected); }); }); }); ================================================ FILE: src/js/__tests__/createInstance.test.js ================================================ import './windowMatchMedia.mock'; import { create } from '../index.js'; describe('create instance', () => { test('without parameters', () => { expect(create()).toBeDefined(); }); test('with options object only', () => { expect( create({ instantUpload: false, }).instantUpload ).toBe(false); }); test('with element only', () => { const form = document.createElement('form'); const input = document.createElement('input'); input.type = 'file'; form.appendChild(input); expect(create(input).element.parentNode).toBe(form); }); test('with element and options object', () => { const form = document.createElement('form'); const input = document.createElement('input'); input.type = 'file'; input.dataset.dropOnPage = false; form.appendChild(input); const pond = create(input, { dropOnPage: true, }); expect(pond.dropOnPage).toBe(false); }); }); ================================================ FILE: src/js/__tests__/removeFile.test.js ================================================ import './windowMatchMedia.mock'; import { create, isSupported } from '../index.js'; const server = { process: (fieldName, file, metadata, load, error, progress, abort) => { let p = 0; const interval = setInterval(() => { p += 0.01; progress(true, p, 1); }, 50); setTimeout(() => { clearInterval(interval); progress(true, 1, 1); load(Date.now()); }, 750); }, }; describe('removing files', () => { let pond = null; const DUMMY_FILE = new File(['Hello World!'], 'text_file_a.txt', { type: 'text/plain', lastModified: new Date(), }); const LOCAL_FILE = { source: '12345', options: { type: 'local', file: { name: 'my-file.png', size: 12345, type: 'image/png', }, }, }; beforeEach(() => { if (pond) { pond.destroy(); } pond = create({ instantUpload: false, server, }); Object.defineProperty(pond.element, 'offsetParent', { get: jest.fn(() => 1), }); }); test('remove file object', () => { pond.files = [DUMMY_FILE]; pond.removeFile(); expect(pond.getFiles().length).toBe(0); }); test('process, then remove file object', done => { pond.onremovefile = (error, file) => { expect(error).toBe(null); expect(pond.getFiles().length).toBe(0); done(); }; pond.onaddfile = () => { pond.processFile().then(() => { pond.removeFile(); }); }; pond.files = [DUMMY_FILE]; }); test('remove file object from client and from server', done => { pond.server = { ...server, remove: (source, load, error) => { setTimeout(() => { load(); }, 10); }, }; pond.onremovefile = (error, file) => { expect(error).toBe(null); expect(pond.getFiles().length).toBe(0); done(); }; pond.onaddfile = () => { pond.removeFile(); }; pond.files = [LOCAL_FILE]; }); test('remove file object from client and fail to remove from server', done => { pond.server = { ...server, remove: (source, load, error) => { setTimeout(() => { error('fail'); }, 10); }, }; const onremovefile = jest.fn(); pond.onremovefile = onremovefile; pond.onremovefile = (error, file) => { expect(error.type).toBe('error'); expect(onremovefile).not.toHaveBeenCalled(); expect(pond.getFiles().length).toBe(1); done(); }; pond.onaddfile = () => { pond.removeFile(); }; pond.files = [LOCAL_FILE]; }); }); ================================================ FILE: src/js/__tests__/revertUploadOnRemove.test.js ================================================ import "./windowMatchMedia.mock"; import { create } from '../index.js'; import { actions } from '../app/actions.js'; /** * These tests verify that revert calls are made to revert uploads on removal of * files from the UI in certain circumstances. Reverting of uploads is generally * handled directly through the REVERT_ITEM_PROCESSING action which is triggered * when clicking the 'x' button that appears to the right hand side of a successful * file upload entry in the filepond UI. There are some cases, such as with * failed chunked uploads, where it's necessary to call revert in response to * a REMOVE_ITEM action, triggered through the remove button the 'x' that appears * to the left hand side of a failed file upload entry. In these cases, the * REVERT_ITEM_PROCESSING action is not triggered and reverting is handled within * REMOVE_ITEM. */ describe('reverting file uploads on remove', () => { const server = { remove: (source, load, error) => { } }; let pond = null; let item = null; // Create 16k of random data to test chunked uploads const fileData = []; for (let i = 0; i < 16384; i++) { fileData.push(Math.floor(Math.random() * 128)); } const TEXT_FILE = new File(['Hello World!'], 'text_file_a.txt', { type: 'text/plain', lastModified: new Date() }); const TEXT_FILE_LARGE = new File(fileData, 'text_file_b.txt', { type: 'text/plain', lastModified: new Date() }); const setupPond = (options, file, fileType = 'input') => { pond = create({ ...options, allowMultiple: false, server }); // Set up an uploaded file with FileOrigin set to INPUT pond.files = [{ source: file, options: { type: fileType } }]; item = pond.getFiles()[0]; // Mock the revert function on item so we can check it's been called item.revert = jest.fn(); // enables draw loop, else it seems that filepond is hidden Object.defineProperty(pond.element, 'offsetParent', { get: jest.fn(() => 1) }); return pond; }; beforeEach(() => { item = null; if (pond) { pond.destroy(); } }); test('no revert on removal of standard upload with chunks disabled', done => { pond = setupPond({ chunkUploads: false }, TEXT_FILE); pond.onremovefile = (error, file) => { expect(item.revert).not.toHaveBeenCalled(); expect(error).toBe(null); expect(pond.getFiles().length).toBe(0); done(); } pond.removeFile(item); }); test('no revert on removal of single chunk upload with chunks enabled', done => { pond = setupPond({ chunkUploads: true, chunkSize: 1024 }, TEXT_FILE); pond.onremovefile = (error, file) => { expect(item.revert).not.toHaveBeenCalled(); expect(error).toBe(null); expect(pond.getFiles().length).toBe(0); done(); } pond.removeFile(item); }); test('revert on removal of single chunk upload with chunkForce set', done => { pond = setupPond({ chunkUploads: true, chunkForce: true, chunkSize: 1024}, TEXT_FILE); pond.onremovefile = (error, file) => { expect(item.revert).toHaveBeenCalledWith(expect.any(Function), false); expect(error).toBe(null); expect(pond.getFiles().length).toBe(0); done(); } pond.removeFile(item); }); test('revert on removal of chunked upload without chunkForce', done => { pond = setupPond({ chunkUploads: true, chunkForce: false, chunkSize: 1024 }, TEXT_FILE_LARGE); pond.onremovefile = (error, file) => { expect(item.revert).toHaveBeenCalledWith(expect.any(Function), false); expect(error).toBe(null); expect(pond.getFiles().length).toBe(0); done(); } pond.removeFile(item); }); test('revert on removal of chunked upload with chunkForce set', () => { pond = setupPond({ chunkUploads: true, chunkForce: true, chunkSize: 1024 }, TEXT_FILE_LARGE); pond.onremovefile = (error, file) => { expect(item.revert).toHaveBeenCalledWith(expect.any(Function), false); expect(error).toBe(null); expect(pond.getFiles().length).toBe(0); done(); } pond.removeFile(item); }); test('revert limbo with serverId set', () => { const fileId = 'abcdefghijklmnop'; pond = setupPond({ chunkUploads: false }, fileId, 'limbo'); // Check that serverId is not null (anything accepts values that are not null or undefined) expect(item.serverId).toBe(fileId); pond.onremovefile = (error, file) => { expect(item.revert).toHaveBeenCalledWith(expect.any(Function), false); expect(error).toBe(null); expect(pond.getFiles().length).toBe(0); done(); } pond.removeFile(item); }); }); ================================================ FILE: src/js/__tests__/server.test.js ================================================ import './windowMatchMedia.mock'; import { create } from '../index.js'; describe('setting server property', () => { let pond; beforeEach(() => { // new pond for each test if (pond) pond.destroy(); pond = create(); // enables draw loop, else it seems that filepond is hidden Object.defineProperty(pond.element, 'offsetParent', { get: jest.fn(() => 1), }); }); test('add fetch headers', () => { pond.server = { headers: { foo: 'bar', }, revert: { headers: { foo: 'baz', }, }, }; expect(pond.server).toMatchObject({ url: '', timeout: 0, process: null, patch: { url: '?patch=', method: 'PATCH', headers: { foo: 'bar' }, withCredentials: false, timeout: 0, onload: null, ondata: null, onerror: null, }, revert: { url: '', method: 'DELETE', headers: { foo: 'baz' }, withCredentials: false, timeout: 0, onload: null, ondata: null, onerror: null, }, fetch: { url: '?fetch=', method: 'GET', headers: { foo: 'bar' }, withCredentials: false, timeout: 0, onload: null, ondata: null, onerror: null, }, restore: { url: '?restore=', method: 'GET', headers: { foo: 'bar' }, withCredentials: false, timeout: 0, onload: null, ondata: null, onerror: null, }, load: { url: '?load=', method: 'GET', headers: { foo: 'bar' }, withCredentials: false, timeout: 0, onload: null, ondata: null, onerror: null, }, remove: null, }); }); }); ================================================ FILE: src/js/__tests__/setFiles.test.js ================================================ import './windowMatchMedia.mock'; import { create } from '../index.js'; const next = cb => { setTimeout(() => { cb(); }, 20); }; describe('setting the files property', () => { let pond = null; const TEXT_DATAURI_A = 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=='; const TEXT_FILE_A = new File(['Hello World!'], 'text_file_a.txt', { type: 'text/plain', lastModified: new Date(), }); const TEXT_FILE_B = new File(['Hello World!'], 'text_file_b.txt', { type: 'text/plain', lastModified: new Date(), }); beforeEach(() => { if (pond) { pond.destroy(); } pond = create({ allowMultiple: true, }); // enables draw loop, else it seems that filepond is hidden Object.defineProperty(pond.element, 'offsetParent', { get: jest.fn(() => 1), }); }); test('set single file object', () => { pond.files = [TEXT_FILE_A]; expect(pond.getFiles().length).toBe(1); }); test('remove single file object', () => { pond.files = [TEXT_FILE_A]; pond.files = []; expect(pond.getFiles().length).toBe(0); }); test('replace single file object', () => { pond.files = [TEXT_FILE_A]; pond.files = [TEXT_FILE_B]; expect(pond.getFile().filename).toBe('text_file_b.txt'); }); test('re-add own file object', done => { // set data uri pond.files = [TEXT_DATAURI_A]; // set marker pond.getFile().setMetadata('marker', 'hello'); next(() => { // update files array with created file object from input file pond.files = [pond.getFile().file]; // expect FilePond to find that this file already exists in array expect(pond.getFile().getMetadata('marker')).toBe('hello'); done(); }); }); test('replace file in list of multiple files', done => { // set data uri pond.files = [TEXT_DATAURI_A, TEXT_FILE_A]; // set marker pond.getFile().setMetadata('marker', 'hello'); next(() => { // update files array with created file object from input file pond.files = [TEXT_FILE_B, pond.getFile().file]; // expect FilePond to find that this file already exists in array expect(pond.getFile(1).getMetadata('marker')).toBe('hello'); done(); }); }); }); ================================================ FILE: src/js/__tests__/windowMatchMedia.mock ================================================ // Mocking window.matchMedia as described in the jest docks // since this is not defined in JSDOM and causes the tests // to fail. See: "https://jestjs.io/docs/en/manual- // mocks#mocking-methods-which-are-not-implemented-in-jsdom" Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ matches: false, media: query, onchange: null, addListener: jest.fn(), // deprecated removeListener: jest.fn(), // deprecated addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), })), }); ================================================ FILE: src/js/app/actions.js ================================================ import { isEmpty } from '../utils/isEmpty'; import { forin } from '../utils/forin'; import { fromCamels } from '../utils/fromCamels'; import { hasRoomForItem } from './utils/hasRoomForItem'; import { insertItem } from './utils/insertItem'; import { createFileLoader } from './utils/createFileLoader'; import { createFetchFunction } from './utils/createFetchFunction'; import { createProcessorFunction } from './utils/createProcessorFunction'; import { createRevertFunction } from './utils/createRevertFunction'; import { createFileProcessor } from './utils/createFileProcessor'; import { createItem } from './utils/createItem'; import { ItemStatus } from './enum/ItemStatus'; import { FileOrigin } from './enum/FileOrigin'; import { getItemById } from './utils/getItemById'; import { getItemByQuery } from './utils/getItemByQuery'; import { InteractionMethod } from './enum/InteractionMethod'; import { applyFilterChain, applyFilters } from '../filter'; import { createItemAPI } from './utils/createItemAPI'; import { createResponse } from '../utils/createResponse'; import { fetchBlob } from './utils/fetchBlob'; import { isExternalURL } from '../utils/isExternalURL'; import { isString } from '../utils/isString'; import { isFile } from '../utils/isFile'; import { dynamicLabel } from './utils/dynamicLabel'; import { getActiveItems } from './utils/getActiveItems'; import { isFunction } from '../utils/isFunction'; import { limit } from '../utils/limit'; const isMockItem = item => !isFile(item.file); const listUpdated = (dispatch, state) => { clearTimeout(state.listUpdateTimeout); state.listUpdateTimeout = setTimeout(() => { dispatch('DID_UPDATE_ITEMS', { items: getActiveItems(state.items) }); }, 0); }; const optionalPromise = (fn, ...params) => new Promise(resolve => { if (!fn) { return resolve(true); } const result = fn(...params); if (result == null) { return resolve(true); } if (typeof result === 'boolean') { return resolve(result); } if (typeof result.then === 'function') { result.then(resolve); } }); const sortItems = (state, compare) => { state.items.sort((a, b) => compare(createItemAPI(a), createItemAPI(b))); }; // returns item based on state const getItemByQueryFromState = (state, itemHandler) => ({ query, success = () => {}, failure = () => {}, ...options } = {}) => { const item = getItemByQuery(state.items, query); if (!item) { failure({ error: createResponse('error', 0, 'Item not found'), file: null, }); return; } itemHandler(item, success, failure, options || {}); }; export const actions = (dispatch, query, state) => ({ /** * Aborts all ongoing processes */ ABORT_ALL: () => { getActiveItems(state.items).forEach(item => { item.freeze(); item.abortLoad(); item.abortProcessing(); }); }, /** * Sets initial files */ DID_SET_FILES: ({ value = [] }) => { // map values to file objects const files = value.map(file => ({ source: file.source ? file.source : file, options: file.options, })); // loop over files, if file is in list, leave it be, if not, remove // test if items should be moved let activeItems = getActiveItems(state.items); activeItems.forEach(item => { // if item not is in new value, remove if (!files.find(file => file.source === item.source || file.source === item.file)) { dispatch('REMOVE_ITEM', { query: item, remove: false }); } }); // add new files activeItems = getActiveItems(state.items); files.forEach((file, index) => { // if file is already in list if (activeItems.find(item => item.source === file.source || item.file === file.source)) return; // not in list, add dispatch('ADD_ITEM', { ...file, interactionMethod: InteractionMethod.NONE, index, }); }); }, DID_UPDATE_ITEM_METADATA: ({ id, action, change }) => { // don't do anything if (change.silent) return; // if is called multiple times in close succession we combined all calls together to save resources clearTimeout(state.itemUpdateTimeout); state.itemUpdateTimeout = setTimeout(() => { const item = getItemById(state.items, id); // only revert and attempt to upload when we're uploading to a server if (!query('IS_ASYNC')) { // should we update the output data applyFilterChain('SHOULD_PREPARE_OUTPUT', false, { item, query, action, change, }).then(shouldPrepareOutput => { // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook const beforePrepareFile = query('GET_BEFORE_PREPARE_FILE'); if (beforePrepareFile) shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput); if (!shouldPrepareOutput) return; dispatch( 'REQUEST_PREPARE_OUTPUT', { query: id, item, success: file => { dispatch('DID_PREPARE_OUTPUT', { id, file }); }, }, true ); }); return; } // if is local item we need to enable upload button so change can be propagated to server if (item.origin === FileOrigin.LOCAL) { dispatch('DID_LOAD_ITEM', { id: item.id, error: null, serverFileReference: item.source, }); } // for async scenarios const upload = () => { // we push this forward a bit so the interface is updated correctly setTimeout(() => { dispatch('REQUEST_ITEM_PROCESSING', { query: id }); }, 32); }; const revert = doUpload => { item.revert( createRevertFunction(state.options.server.url, state.options.server.revert), query('GET_FORCE_REVERT') ) .then(doUpload ? upload : () => {}) .catch(() => {}); }; const abort = doUpload => { item.abortProcessing().then(doUpload ? upload : () => {}); }; // if we should re-upload the file immediately if (item.status === ItemStatus.PROCESSING_COMPLETE) { return revert(state.options.instantUpload); } // if currently uploading, cancel upload if (item.status === ItemStatus.PROCESSING) { return abort(state.options.instantUpload); } if (state.options.instantUpload) { upload(); } }, 0); }, MOVE_ITEM: ({ query, index }) => { const item = getItemByQuery(state.items, query); if (!item) return; const currentIndex = state.items.indexOf(item); index = limit(index, 0, state.items.length - 1); if (currentIndex === index) return; state.items.splice(index, 0, state.items.splice(currentIndex, 1)[0]); }, SORT: ({ compare }) => { sortItems(state, compare); dispatch('DID_SORT_ITEMS', { items: query('GET_ACTIVE_ITEMS'), }); }, ADD_ITEMS: ({ items, index, interactionMethod, success = () => {}, failure = () => {} }) => { let currentIndex = index; if (index === -1 || typeof index === 'undefined') { const insertLocation = query('GET_ITEM_INSERT_LOCATION'); const totalItems = query('GET_TOTAL_ITEMS'); currentIndex = insertLocation === 'before' ? 0 : totalItems; } const ignoredFiles = query('GET_IGNORED_FILES'); const isValidFile = source => isFile(source) ? !ignoredFiles.includes(source.name.toLowerCase()) : !isEmpty(source); const validItems = items.filter(isValidFile); const promises = validItems.map( source => new Promise((resolve, reject) => { dispatch('ADD_ITEM', { interactionMethod, source: source.source || source, success: resolve, failure: reject, index: currentIndex++, options: source.options || {}, }); }) ); Promise.all(promises) .then(success) .catch(failure); }, /** * @param source * @param index * @param interactionMethod */ ADD_ITEM: ({ source, index = -1, interactionMethod, success = () => {}, failure = () => {}, options = {}, }) => { // if no source supplied if (isEmpty(source)) { failure({ error: createResponse('error', 0, 'No source'), file: null, }); return; } // filter out invalid file items, used to filter dropped directory contents if (isFile(source) && state.options.ignoredFiles.includes(source.name.toLowerCase())) { // fail silently return; } // test if there's still room in the list of files if (!hasRoomForItem(state)) { // if multiple allowed, we can't replace // or if only a single item is allowed but we're not allowed to replace it we exit if ( state.options.allowMultiple || (!state.options.allowMultiple && !state.options.allowReplace) ) { const error = createResponse('warning', 0, 'Max files'); dispatch('DID_THROW_MAX_FILES', { source, error, }); failure({ error, file: null }); return; } // let's replace the item // id of first item we're about to remove const item = getActiveItems(state.items)[0]; // if has been processed remove it from the server as well if ( item.status === ItemStatus.PROCESSING_COMPLETE || item.status === ItemStatus.PROCESSING_REVERT_ERROR ) { const forceRevert = query('GET_FORCE_REVERT'); item.revert( createRevertFunction(state.options.server.url, state.options.server.revert), forceRevert ) .then(() => { if (!forceRevert) return; // try to add now dispatch('ADD_ITEM', { source, index, interactionMethod, success, failure, options, }); }) .catch(() => {}); // no need to handle this catch state for now if (forceRevert) return; } // remove first item as it will be replaced by this item dispatch('REMOVE_ITEM', { query: item.id }); } // where did the file originate const origin = options.type === 'local' ? FileOrigin.LOCAL : options.type === 'limbo' ? FileOrigin.LIMBO : FileOrigin.INPUT; // create a new blank item const item = createItem( // where did this file come from origin, // an input file never has a server file reference origin === FileOrigin.INPUT ? null : source, // file mock data, if defined options.file ); // set initial meta data Object.keys(options.metadata || {}).forEach(key => { item.setMetadata(key, options.metadata[key]); }); // created the item, let plugins add methods applyFilters('DID_CREATE_ITEM', item, { query, dispatch }); // where to insert new items const itemInsertLocation = query('GET_ITEM_INSERT_LOCATION'); // adjust index if is not allowed to pick location if (!state.options.itemInsertLocationFreedom) { index = itemInsertLocation === 'before' ? -1 : state.items.length; } // add item to list insertItem(state.items, item, index); // sort items in list if (isFunction(itemInsertLocation) && source) { sortItems(state, itemInsertLocation); } // get a quick reference to the item id const id = item.id; // observe item events item.on('init', () => { dispatch('DID_INIT_ITEM', { id }); }); item.on('load-init', () => { dispatch('DID_START_ITEM_LOAD', { id }); }); item.on('load-meta', () => { dispatch('DID_UPDATE_ITEM_META', { id }); }); item.on('load-progress', progress => { dispatch('DID_UPDATE_ITEM_LOAD_PROGRESS', { id, progress }); }); item.on('load-request-error', error => { const mainStatus = dynamicLabel(state.options.labelFileLoadError)(error); // is client error, no way to recover if (error.code >= 400 && error.code < 500) { dispatch('DID_THROW_ITEM_INVALID', { id, error, status: { main: mainStatus, sub: `${error.code} (${error.body})`, }, }); // reject the file so can be dealt with through API failure({ error, file: createItemAPI(item) }); return; } // is possible server error, so might be possible to retry dispatch('DID_THROW_ITEM_LOAD_ERROR', { id, error, status: { main: mainStatus, sub: state.options.labelTapToRetry, }, }); }); item.on('load-file-error', error => { dispatch('DID_THROW_ITEM_INVALID', { id, error: error.status, status: error.status, }); failure({ error: error.status, file: createItemAPI(item) }); }); item.on('load-abort', () => { dispatch('REMOVE_ITEM', { query: id }); }); item.on('load-skip', () => { item.on('metadata-update', change => { if (!isFile(item.file)) return; dispatch('DID_UPDATE_ITEM_METADATA', { id, change }); }); dispatch('COMPLETE_LOAD_ITEM', { query: id, item, data: { source, success, }, }); }); item.on('load', () => { const handleAdd = shouldAdd => { // no should not add this file if (!shouldAdd) { dispatch('REMOVE_ITEM', { query: id, }); return; } // now interested in metadata updates item.on('metadata-update', change => { dispatch('DID_UPDATE_ITEM_METADATA', { id, change }); }); // let plugins decide if the output data should be prepared at this point // means we'll do this and wait for idle state applyFilterChain('SHOULD_PREPARE_OUTPUT', false, { item, query }).then( shouldPrepareOutput => { // plugins determined the output data should be prepared (or not), can be adjusted with beforePrepareOutput hook const beforePrepareFile = query('GET_BEFORE_PREPARE_FILE'); if (beforePrepareFile) shouldPrepareOutput = beforePrepareFile(item, shouldPrepareOutput); const loadComplete = () => { dispatch('COMPLETE_LOAD_ITEM', { query: id, item, data: { source, success, }, }); listUpdated(dispatch, state); }; // exit if (shouldPrepareOutput) { // wait for idle state and then run PREPARE_OUTPUT dispatch( 'REQUEST_PREPARE_OUTPUT', { query: id, item, success: file => { dispatch('DID_PREPARE_OUTPUT', { id, file }); loadComplete(); }, }, true ); return; } loadComplete(); } ); }; // item loaded, allow plugins to // - read data (quickly) // - add metadata applyFilterChain('DID_LOAD_ITEM', item, { query, dispatch }) .then(() => { optionalPromise(query('GET_BEFORE_ADD_FILE'), createItemAPI(item)).then( handleAdd ); }) .catch(e => { if (!e || !e.error || !e.status) return handleAdd(false); dispatch('DID_THROW_ITEM_INVALID', { id, error: e.error, status: e.status, }); }); }); item.on('process-start', () => { dispatch('DID_START_ITEM_PROCESSING', { id }); }); item.on('process-progress', progress => { dispatch('DID_UPDATE_ITEM_PROCESS_PROGRESS', { id, progress }); }); item.on('process-error', error => { dispatch('DID_THROW_ITEM_PROCESSING_ERROR', { id, error, status: { main: dynamicLabel(state.options.labelFileProcessingError)(error), sub: state.options.labelTapToRetry, }, }); }); item.on('process-revert-error', error => { dispatch('DID_THROW_ITEM_PROCESSING_REVERT_ERROR', { id, error, status: { main: dynamicLabel(state.options.labelFileProcessingRevertError)(error), sub: state.options.labelTapToRetry, }, }); }); item.on('process-complete', serverFileReference => { dispatch('DID_COMPLETE_ITEM_PROCESSING', { id, error: null, serverFileReference, }); dispatch('DID_DEFINE_VALUE', { id, value: serverFileReference }); }); item.on('process-abort', () => { dispatch('DID_ABORT_ITEM_PROCESSING', { id }); }); item.on('process-revert', () => { dispatch('DID_REVERT_ITEM_PROCESSING', { id }); dispatch('DID_DEFINE_VALUE', { id, value: null }); }); // let view know the item has been inserted dispatch('DID_ADD_ITEM', { id, index, interactionMethod }); listUpdated(dispatch, state); // start loading the source const { url, load, restore, fetch } = state.options.server || {}; item.load( source, // this creates a function that loads the file based on the type of file (string, base64, blob, file) and location of file (local, remote, limbo) createFileLoader( origin === FileOrigin.INPUT ? // input, if is remote, see if should use custom fetch, else use default fetchBlob isString(source) && isExternalURL(source) ? fetch ? createFetchFunction(url, fetch) : fetchBlob // remote url : fetchBlob // try to fetch url : // limbo or local origin === FileOrigin.LIMBO ? createFetchFunction(url, restore) // limbo : createFetchFunction(url, load) // local ), // called when the file is loaded so it can be piped through the filters (file, success, error) => { // let's process the file applyFilterChain('LOAD_FILE', file, { query }) .then(success) .catch(error); } ); }, REQUEST_PREPARE_OUTPUT: ({ item, success, failure = () => {} }) => { // error response if item archived const err = { error: createResponse('error', 0, 'Item not found'), file: null, }; // don't handle archived items, an item could have been archived (load aborted) while waiting to be prepared if (item.archived) return failure(err); // allow plugins to alter the file data applyFilterChain('PREPARE_OUTPUT', item.file, { query, item }).then(result => { applyFilterChain('COMPLETE_PREPARE_OUTPUT', result, { query, item }).then(result => { // don't handle archived items, an item could have been archived (load aborted) while being prepared if (item.archived) return failure(err); // we done! success(result); }); }); }, COMPLETE_LOAD_ITEM: ({ item, data }) => { const { success, source } = data; // sort items in list const itemInsertLocation = query('GET_ITEM_INSERT_LOCATION'); if (isFunction(itemInsertLocation) && source) { sortItems(state, itemInsertLocation); } // let interface know the item has loaded dispatch('DID_LOAD_ITEM', { id: item.id, error: null, serverFileReference: item.origin === FileOrigin.INPUT ? null : source, }); // item has been successfully loaded and added to the // list of items so can now be safely returned for use success(createItemAPI(item)); // if this is a local server file we need to show a different state if (item.origin === FileOrigin.LOCAL) { dispatch('DID_LOAD_LOCAL_ITEM', { id: item.id }); return; } // if is a temp server file we prevent async upload call here (as the file is already on the server) if (item.origin === FileOrigin.LIMBO) { dispatch('DID_COMPLETE_ITEM_PROCESSING', { id: item.id, error: null, serverFileReference: source, }); dispatch('DID_DEFINE_VALUE', { id: item.id, value: item.serverId || source, }); return; } // id we are allowed to upload the file immediately, lets do it if (query('IS_ASYNC') && state.options.instantUpload) { dispatch('REQUEST_ITEM_PROCESSING', { query: item.id }); } }, RETRY_ITEM_LOAD: getItemByQueryFromState(state, item => { // try loading the source one more time item.retryLoad(); }), REQUEST_ITEM_PREPARE: getItemByQueryFromState(state, (item, success, failure) => { dispatch( 'REQUEST_PREPARE_OUTPUT', { query: item.id, item, success: file => { dispatch('DID_PREPARE_OUTPUT', { id: item.id, file }); success({ file: item, output: file, }); }, failure, }, true ); }), REQUEST_ITEM_PROCESSING: getItemByQueryFromState(state, (item, success, failure) => { // cannot be queued (or is already queued) const itemCanBeQueuedForProcessing = // waiting for something item.status === ItemStatus.IDLE || // processing went wrong earlier item.status === ItemStatus.PROCESSING_ERROR; // not ready to be processed if (!itemCanBeQueuedForProcessing) { const processNow = () => dispatch('REQUEST_ITEM_PROCESSING', { query: item, success, failure }); const process = () => (document.hidden ? processNow() : setTimeout(processNow, 32)); // if already done processing or tried to revert but didn't work, try again if ( item.status === ItemStatus.PROCESSING_COMPLETE || item.status === ItemStatus.PROCESSING_REVERT_ERROR ) { item.revert( createRevertFunction(state.options.server.url, state.options.server.revert), query('GET_FORCE_REVERT') ) .then(process) .catch(() => {}); // don't continue with processing if something went wrong } else if (item.status === ItemStatus.PROCESSING) { item.abortProcessing().then(process); } return; } // already queued for processing if (item.status === ItemStatus.PROCESSING_QUEUED) return; item.requestProcessing(); dispatch('DID_REQUEST_ITEM_PROCESSING', { id: item.id }); dispatch('PROCESS_ITEM', { query: item, success, failure }, true); }), PROCESS_ITEM: getItemByQueryFromState(state, (item, success, failure) => { const maxParallelUploads = query('GET_MAX_PARALLEL_UPLOADS'); const totalCurrentUploads = query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING).length; // queue and wait till queue is freed up if (totalCurrentUploads === maxParallelUploads) { // queue for later processing state.processingQueue.push({ id: item.id, success, failure, }); // stop it! return; } // if was not queued or is already processing exit here if (item.status === ItemStatus.PROCESSING) return; const processNext = () => { // process queueud items const queueEntry = state.processingQueue.shift(); // no items left if (!queueEntry) return; // get item reference const { id, success, failure } = queueEntry; const itemReference = getItemByQuery(state.items, id); // if item was archived while in queue, jump to next if (!itemReference || itemReference.archived) { processNext(); return; } // process queued item dispatch('PROCESS_ITEM', { query: id, success, failure }, true); }; // we done function item.onOnce('process-complete', () => { success(createItemAPI(item)); processNext(); // if origin is local, and we're instant uploading, trigger remove of original // as revert will remove file from list const server = state.options.server; const instantUpload = state.options.instantUpload; if (instantUpload && item.origin === FileOrigin.LOCAL && isFunction(server.remove)) { const noop = () => {}; item.origin = FileOrigin.LIMBO; state.options.server.remove(item.source, noop, noop); } // All items processed? No errors? const allItemsProcessed = query('GET_ITEMS_BY_STATUS', ItemStatus.PROCESSING_COMPLETE).length === state.items.length; if (allItemsProcessed) { dispatch('DID_COMPLETE_ITEM_PROCESSING_ALL'); } }); // we error function item.onOnce('process-error', error => { failure({ error, file: createItemAPI(item) }); processNext(); }); // abort function item.onOnce('process-abort', () => { processNext(); }); // start file processing const options = state.options; item.process( createFileProcessor( createProcessorFunction(options.server.url, options.server.process, options.name, { chunkTransferId: item.transferId, chunkServer: options.server.patch, chunkUploads: options.chunkUploads, chunkForce: options.chunkForce, chunkSize: options.chunkSize, chunkRetryDelays: options.chunkRetryDelays, }), { allowMinimumUploadDuration: query('GET_ALLOW_MINIMUM_UPLOAD_DURATION'), } ), // called when the file is about to be processed so it can be piped through the transform filters (file, success, error) => { // allow plugins to alter the file data applyFilterChain('PREPARE_OUTPUT', file, { query, item }) .then(file => { dispatch('DID_PREPARE_OUTPUT', { id: item.id, file }); success(file); }) .catch(error); } ); }), RETRY_ITEM_PROCESSING: getItemByQueryFromState(state, item => { dispatch('REQUEST_ITEM_PROCESSING', { query: item }); }), REQUEST_REMOVE_ITEM: getItemByQueryFromState(state, item => { optionalPromise(query('GET_BEFORE_REMOVE_FILE'), createItemAPI(item)).then(shouldRemove => { if (!shouldRemove) { return; } dispatch('REMOVE_ITEM', { query: item }); }); }), RELEASE_ITEM: getItemByQueryFromState(state, item => { item.release(); }), REMOVE_ITEM: getItemByQueryFromState(state, (item, success, failure, options) => { const removeFromView = () => { // get id reference const id = item.id; // archive the item, this does not remove it from the list getItemById(state.items, id).archive(); // tell the view the item has been removed dispatch('DID_REMOVE_ITEM', { error: null, id, item }); // now the list has been modified listUpdated(dispatch, state); // correctly removed success(createItemAPI(item)); }; // if this is a local file and the `server.remove` function has been configured, // send source there so dev can remove file from server const server = state.options.server; if ( item.origin === FileOrigin.LOCAL && server && isFunction(server.remove) && options.remove !== false ) { dispatch('DID_START_ITEM_REMOVE', { id: item.id }); server.remove( item.source, () => removeFromView(), status => { dispatch('DID_THROW_ITEM_REMOVE_ERROR', { id: item.id, error: createResponse('error', 0, status, null), status: { main: dynamicLabel(state.options.labelFileRemoveError)(status), sub: state.options.labelTapToRetry, }, }); } ); } else { // if is requesting revert and can revert need to call revert handler (not calling request_ because that would also trigger beforeRemoveHook) if ( (options.revert && item.origin !== FileOrigin.LOCAL && item.serverId !== null) || // if chunked uploads are enabled and we're uploading in chunks for this specific file // or if the file isn't big enough for chunked uploads but chunkForce is set then call // revert before removing from the view... (state.options.chunkUploads && item.file.size > state.options.chunkSize) || (state.options.chunkUploads && state.options.chunkForce) ) { item.revert( createRevertFunction(state.options.server.url, state.options.server.revert), query('GET_FORCE_REVERT') ); } // can now safely remove from view removeFromView(); } }), ABORT_ITEM_LOAD: getItemByQueryFromState(state, item => { item.abortLoad(); }), ABORT_ITEM_PROCESSING: getItemByQueryFromState(state, item => { // test if is already processed if (item.serverId) { dispatch('REVERT_ITEM_PROCESSING', { id: item.id }); return; } // abort item.abortProcessing().then(() => { const shouldRemove = state.options.instantUpload; if (shouldRemove) { dispatch('REMOVE_ITEM', { query: item.id }); } }); }), REQUEST_REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, item => { // not instant uploading, revert immediately if (!state.options.instantUpload) { dispatch('REVERT_ITEM_PROCESSING', { query: item }); return; } // if we're instant uploading the file will also be removed if we revert, // so if a before remove file hook is defined we need to run it now const handleRevert = shouldRevert => { if (!shouldRevert) return; dispatch('REVERT_ITEM_PROCESSING', { query: item }); }; const fn = query('GET_BEFORE_REMOVE_FILE'); if (!fn) { return handleRevert(true); } const requestRemoveResult = fn(createItemAPI(item)); if (requestRemoveResult == null) { // undefined or null return handleRevert(true); } if (typeof requestRemoveResult === 'boolean') { return handleRevert(requestRemoveResult); } if (typeof requestRemoveResult.then === 'function') { requestRemoveResult.then(handleRevert); } }), REVERT_ITEM_PROCESSING: getItemByQueryFromState(state, item => { item.revert( createRevertFunction(state.options.server.url, state.options.server.revert), query('GET_FORCE_REVERT') ) .then(() => { const shouldRemove = state.options.instantUpload || isMockItem(item); if (shouldRemove) { dispatch('REMOVE_ITEM', { query: item.id }); } }) .catch(() => {}); }), SET_OPTIONS: ({ options }) => { // get all keys passed const optionKeys = Object.keys(options); // get prioritized keyed to include (remove once not in options object) const prioritizedOptionKeys = PrioritizedOptions.filter(key => optionKeys.includes(key)); // order the keys, prioritized first, then rest const orderedOptionKeys = [ // add prioritized first if passed to options, else remove ...prioritizedOptionKeys, // prevent duplicate keys ...Object.keys(options).filter(key => !prioritizedOptionKeys.includes(key)), ]; // dispatch set event for each option orderedOptionKeys.forEach(key => { dispatch(`SET_${fromCamels(key, '_').toUpperCase()}`, { value: options[key], }); }); }, }); const PrioritizedOptions = [ 'server', // must be processed before "files" ]; ================================================ FILE: src/js/app/enum/ChunkStatus.js ================================================ export const ChunkStatus = { QUEUED: 0, COMPLETE: 1, PROCESSING: 2, ERROR: 3, WAITING: 4 }; ================================================ FILE: src/js/app/enum/FileOrigin.js ================================================ export const FileOrigin = { INPUT:1, LIMBO:2, LOCAL:3 }; ================================================ FILE: src/js/app/enum/InteractionMethod.js ================================================ export const InteractionMethod = { API: 1, DROP: 2, BROWSE: 3, PASTE: 4, NONE: 5 }; ================================================ FILE: src/js/app/enum/ItemStatus.js ================================================ export const ItemStatus = { INIT: 1, IDLE: 2, PROCESSING_QUEUED: 9, PROCESSING: 3, PROCESSING_COMPLETE: 5, PROCESSING_ERROR: 6, PROCESSING_REVERT_ERROR: 10, LOADING: 7, LOAD_ERROR: 8 }; ================================================ FILE: src/js/app/enum/Key.js ================================================ export const Key = { ENTER: 13, SPACE: 32 }; ================================================ FILE: src/js/app/enum/Status.js ================================================ export const Status = { EMPTY: 0, IDLE: 1, // waiting ERROR: 2, // a file is in error state BUSY: 3, // busy processing or loading READY: 4 // all files uploaded }; ================================================ FILE: src/js/app/enum/Type.js ================================================ export const Type = { BOOLEAN: 'boolean', INT: 'int', NUMBER: 'number', STRING: 'string', ARRAY: 'array', OBJECT: 'object', FUNCTION: 'function', ACTION: 'action', SERVER_API: 'serverapi', REGEX: 'regex' }; ================================================ FILE: src/js/app/frame/createPainter.js ================================================ export const createPainter = (read, write, fps = 60) => { const name = '__framePainter'; // set global painter if (window[name]) { window[name].readers.push(read); window[name].writers.push(write); return; } window[name] = { readers:[read], writers:[write] } const painter = window[name]; const interval = 1000 / fps; let last = null; let id = null; let requestTick = null; let cancelTick = null; const setTimerType = () => { if (document.hidden) { requestTick = () => window.setTimeout(() => tick(performance.now()), interval); cancelTick = () => window.clearTimeout(id); } else { requestTick = () => window.requestAnimationFrame(tick); cancelTick = () => window.cancelAnimationFrame(id); } } document.addEventListener('visibilitychange', () => { if (cancelTick) cancelTick(); setTimerType(); tick(performance.now()); }); const tick = ts => { // queue next tick id = requestTick(tick); // limit fps if (!last) { last = ts; } const delta = ts - last; if (delta <= interval) { // skip frame return; } // align next frame last = ts - delta % interval; // update view painter.readers.forEach(read => read()); painter.writers.forEach(write => write(ts)); }; setTimerType(); tick(performance.now()); return { pause: () => { cancelTick(id); } }; }; ================================================ FILE: src/js/app/frame/createRoute.js ================================================ export const createRoute = (routes, fn) => ({ root, props, actions = [], timestamp, shouldOptimize }) => { actions.filter(action => routes[action.type]) .forEach(action => routes[action.type]({ root, props, action: action.data, timestamp, shouldOptimize }) ); if (fn) { fn({ root, props, actions, timestamp, shouldOptimize }); }; }; ================================================ FILE: src/js/app/frame/createStore.js ================================================ export const createStore = (initialState, queries = [], actions = []) => { // internal state const state = { ...initialState }; // contains all actions for next frame, is clear when actions are requested const actionQueue = []; const dispatchQueue = []; // returns a duplicate of the current state const getState = () => ({ ...state }); // returns a duplicate of the actions array and clears the actions array const processActionQueue = () => { // create copy of actions queue const queue = [...actionQueue]; // clear actions queue (we don't want no double actions) actionQueue.length = 0; return queue; }; // processes actions that might block the main UI thread const processDispatchQueue = () => { // create copy of actions queue const queue = [...dispatchQueue]; // clear actions queue (we don't want no double actions) dispatchQueue.length = 0; // now dispatch these actions queue.forEach(({ type, data }) => { dispatch(type, data); }); }; // adds a new action, calls its handler and const dispatch = (type, data, isBlocking) => { // is blocking action (should never block if document is hidden) if (isBlocking && !document.hidden) { dispatchQueue.push({ type, data }); return; } // if this action has a handler, handle the action if (actionHandlers[type]) { actionHandlers[type](data); } // now add action actionQueue.push({ type, data }); }; const query = (str, ...args) => queryHandles[str] ? queryHandles[str](...args) : null; const api = { getState, processActionQueue, processDispatchQueue, dispatch, query }; let queryHandles = {}; queries.forEach(query => { queryHandles = { ...query(state), ...queryHandles }; }); let actionHandlers = {}; actions.forEach(action => { actionHandlers = { ...action(dispatch, query, state), ...actionHandlers }; }); return api; }; ================================================ FILE: src/js/app/frame/createView.js ================================================ import { createObject } from '../../utils/createObject'; import { createElement } from './utils/createElement'; import { appendChild } from './utils/appendChild'; import { appendChildView } from './utils/appendChildView'; import { removeChildView } from './utils/removeChildView'; import { getChildCount } from './utils/getChildCount'; import { getViewRect } from './utils/getViewRect'; import { Mixins } from './mixins/index'; import { updateRect } from './utils/updateRect'; export const createView = // default view definition ({ // element definition tag = 'div', name = null, attributes = {}, // view interaction read = () => {}, write = () => {}, create = () => {}, destroy = () => {}, // hooks filterFrameActionsForChild = (child, actions) => actions, didCreateView = () => {}, didWriteView = () => {}, // rect related ignoreRect = false, ignoreRectUpdate = false, // mixins mixins = [] } = {}) => ( // each view requires reference to store store, // specific properties for this view props = {} ) => { // root element should not be changed const element = createElement(tag, `filepond--${name}`, attributes); // style reference should also not be changed const style = window.getComputedStyle(element, null); // element rectangle const rect = updateRect(); let frameRect = null; // rest state let isResting = false; // pretty self explanatory const childViews = []; // loaded mixins const activeMixins = []; // references to created children const ref = {}; // state used for each instance const state = {}; // list of writers that will be called to update this view const writers = [ write // default writer ]; const readers = [ read // default reader ]; const destroyers = [ destroy // default destroy ]; // core view methods const getElement = () => element; const getChildViews = () => childViews.concat(); const getReference = () => ref; const createChildView = store => (view, props) => view(store, props); const getRect = () => { if (frameRect) { return frameRect; } frameRect = getViewRect(rect, childViews, [0, 0], [1, 1]) return frameRect; }; const getStyle = () => style; /** * Read data from DOM * @private */ const _read = () => { frameRect = null; // read child views childViews.forEach(child => child._read()); const shouldUpdate = !(ignoreRectUpdate && rect.width && rect.height) if (shouldUpdate) { updateRect(rect, element, style); } // readers const api = { root: internalAPI, props, rect }; readers.forEach(reader => reader(api) ); }; /** * Write data to DOM * @private */ const _write = (ts, frameActions, shouldOptimize) => { // if no actions, we assume that the view is resting let resting = frameActions.length === 0; // writers writers.forEach(writer => { const writerResting = writer({ props, root: internalAPI, actions: frameActions, timestamp: ts, shouldOptimize }); if (writerResting === false) { resting = false; } }); // run mixins activeMixins.forEach(mixin => { // if one of the mixins is still busy after write operation, we are not resting const mixinResting = mixin.write(ts); if (mixinResting === false) { resting = false; } }); // updates child views that are currently attached to the DOM childViews .filter(child => !!child.element.parentNode) .forEach(child => { // if a child view is not resting, we are not resting const childResting = child._write( ts, filterFrameActionsForChild(child, frameActions), shouldOptimize ); if (!childResting) { resting = false; } }); // append new elements to DOM and update those childViews //.filter(child => !child.element.parentNode) .forEach((child, index) => { // skip if (child.element.parentNode) { return; } // append to DOM internalAPI.appendChild(child.element, index); // call read (need to know the size of these elements) child._read(); // re-call write child._write( ts, filterFrameActionsForChild(child, frameActions), shouldOptimize ); // we just added somthing to the dom, no rest resting = false; }); // update resting state isResting = resting; didWriteView({ props, root: internalAPI, actions: frameActions, timestamp: ts }); // let parent know if we are resting return resting; }; const _destroy = () => { activeMixins.forEach(mixin => mixin.destroy()); destroyers.forEach(destroyer => { destroyer({ root: internalAPI, props })}); childViews.forEach(child => child._destroy()); }; // sharedAPI const sharedAPIDefinition = { element: { get: getElement }, style: { get: getStyle }, childViews: { get: getChildViews } }; // private API definition const internalAPIDefinition = { ...sharedAPIDefinition, rect: { get: getRect }, // access to custom children references ref: { get: getReference }, // dom modifiers is: needle => name === needle, appendChild: appendChild(element), createChildView: createChildView(store), linkView: view => { childViews.push(view); return view }, unlinkView: view => { childViews.splice(childViews.indexOf(view), 1); }, appendChildView: appendChildView(element, childViews), removeChildView: removeChildView(element, childViews), registerWriter: writer => writers.push(writer), registerReader: reader => readers.push(reader), registerDestroyer: destroyer => destroyers.push(destroyer), invalidateLayout: () => element.layoutCalculated = false, // access to data store dispatch: store.dispatch, query: store.query }; // public view API methods const externalAPIDefinition = { element: { get: getElement }, childViews: { get: getChildViews }, rect: { get: getRect }, resting: { get: () => isResting }, isRectIgnored: () => ignoreRect, _read, _write, _destroy }; // mixin API methods const mixinAPIDefinition = { ...sharedAPIDefinition, rect: { get: () => rect } }; // add mixin functionality Object.keys(mixins).sort((a,b) => { // move styles to the back of the mixin list (so adjustments of other mixins are applied to the props correctly) if (a === 'styles') { return 1; } else if (b ==='styles') { return -1; } return 0; }).forEach(key => { const mixinAPI = Mixins[key]({ mixinConfig: mixins[key], viewProps: props, viewState: state, viewInternalAPI: internalAPIDefinition, viewExternalAPI: externalAPIDefinition, view: createObject(mixinAPIDefinition) }); if (mixinAPI) { activeMixins.push(mixinAPI); } }) // construct private api const internalAPI = createObject(internalAPIDefinition); // create the view create({ root: internalAPI, props }); // append created child views to root node const childCount = getChildCount(element); // need to know the current child count so appending happens in correct order childViews.forEach((child, index) => { internalAPI.appendChild(child.element, childCount + index); }); // call did create didCreateView(internalAPI); // expose public api return createObject(externalAPIDefinition); }; ================================================ FILE: src/js/app/frame/index.js ================================================ import { createStore } from './createStore'; import { createView } from './createView'; import { createPainter } from './createPainter'; import { createRoute } from './createRoute'; export { createRoute, createPainter, createStore, createView }; ================================================ FILE: src/js/app/frame/mixins/animations.js ================================================ import { createAnimator } from '../utils/createAnimator'; import { forin } from '../../../utils/forin'; import { addGetSet } from './utils/addGetSet'; // add to state, // add getters and setters to internal and external api (if not set) // setup animators export const animations = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI }) => { // initial properties const initialProps = { ...viewProps }; // list of all active animations const animations = []; // setup animators forin(mixinConfig, (property, animation) => { const animator = createAnimator(animation); if (!animator) { return; } // when the animator updates, update the view state value animator.onupdate = value => { viewProps[property] = value; }; // set animator target animator.target = initialProps[property]; // when value is set, set the animator target value const prop = { key: property, setter: value => { // if already at target, we done! if (animator.target === value) { return; } animator.target = value; }, getter: () => viewProps[property] }; // add getters and setters addGetSet([prop], [viewInternalAPI, viewExternalAPI], viewProps, true); // add it to the list for easy updating from the _write method animations.push(animator); }); // expose internal write api return { write: ts => { let skipToEndState = document.hidden; let resting = true; animations.forEach(animation => { if (!animation.resting) resting = false; animation.interpolate(ts, skipToEndState); }); return resting; }, destroy: () => {} }; }; ================================================ FILE: src/js/app/frame/mixins/apis.js ================================================ import { addGetSet } from './utils/addGetSet'; // add to external api and link to props export const apis = ({ mixinConfig, viewProps, viewExternalAPI }) => { addGetSet(mixinConfig, viewExternalAPI, viewProps); }; ================================================ FILE: src/js/app/frame/mixins/index.js ================================================ import { animations } from './animations'; import { listeners } from './listeners'; import { apis } from './apis'; import { styles } from './styles'; export const Mixins = { styles, listeners, animations, apis }; ================================================ FILE: src/js/app/frame/mixins/listeners.js ================================================ import { addEvent } from '../utils/addEvent'; import { removeEvent } from '../utils/removeEvent'; // mixin export const listeners = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI, viewState, view }) => { const events = []; const add = addEvent(view.element); const remove = removeEvent(view.element); viewExternalAPI.on = (type, fn) => { events.push({ type, fn }); add(type, fn); }; viewExternalAPI.off = (type, fn) => { events.splice( events.findIndex(event => event.type === type && event.fn === fn), 1 ); remove(type, fn); }; return { write: () => { // not busy return true; }, destroy: () => { events.forEach(event => { remove(event.type, event.fn); }); } }; }; ================================================ FILE: src/js/app/frame/mixins/styles.js ================================================ import { getViewRect } from '../utils/getViewRect'; import { addGetSet } from './utils/addGetSet'; import { isDefined } from '../../../utils/isDefined'; // add to state, // add getters and setters to internal and external api (if not set) // set initial state based on props in viewProps // apply as transforms each frame const defaults = { opacity: 1, scaleX: 1, scaleY: 1, translateX: 0, translateY: 0, rotateX: 0, rotateY: 0, rotateZ: 0, originX: 0, originY: 0 }; export const styles = ({ mixinConfig, viewProps, viewInternalAPI, viewExternalAPI, view }) => { // initial props const initialProps = { ...viewProps }; // current props const currentProps = {}; // we will add those properties to the external API and link them to the viewState addGetSet(mixinConfig, [viewInternalAPI, viewExternalAPI], viewProps); // override rect on internal and external rect getter so it takes in account transforms const getOffset = () => [ viewProps['translateX'] || 0, viewProps['translateY'] || 0 ]; const getScale = () => [viewProps['scaleX'] || 0, viewProps['scaleY'] || 0]; const getRect = () => view.rect ? getViewRect(view.rect, view.childViews, getOffset(), getScale()) : null; viewInternalAPI.rect = { get: getRect }; viewExternalAPI.rect = { get: getRect }; // apply view props mixinConfig.forEach(key => { viewProps[key] = typeof initialProps[key] === 'undefined' ? defaults[key] : initialProps[key]; }); // expose api return { write: () => { // see if props have changed if (!propsHaveChanged(currentProps, viewProps)) { return; } // moves element to correct position on screen applyStyles(view.element, viewProps); // store new transforms Object.assign(currentProps, {...viewProps}); // no longer busy return true; }, destroy: () => {} }; }; const propsHaveChanged = (currentProps, newProps) => { // different amount of keys if (Object.keys(currentProps).length !== Object.keys(newProps).length) { return true; } // lets analyze the individual props for (const prop in newProps) { if (newProps[prop] !== currentProps[prop]) { return true; } } return false; }; const applyStyles = ( element, { opacity, perspective, translateX, translateY, scaleX, scaleY, rotateX, rotateY, rotateZ, originX, originY, width, height } ) => { let transforms = ''; let styles = ''; // handle transform origin if (isDefined(originX) || isDefined(originY)) { styles += `transform-origin: ${originX || 0}px ${originY || 0}px;`; } // transform order is relevant // 0. perspective if (isDefined(perspective)) { transforms += `perspective(${perspective}px) `; } // 1. translate if (isDefined(translateX) || isDefined(translateY)) { transforms += `translate3d(${translateX || 0}px, ${translateY || 0}px, 0) `; } // 2. scale if (isDefined(scaleX) || isDefined(scaleY)) { transforms += `scale3d(${isDefined(scaleX) ? scaleX : 1}, ${ isDefined(scaleY) ? scaleY : 1 }, 1) `; } // 3. rotate if (isDefined(rotateZ)) { transforms += `rotateZ(${rotateZ}rad) `; } if (isDefined(rotateX)) { transforms += `rotateX(${rotateX}rad) `; } if (isDefined(rotateY)) { transforms += `rotateY(${rotateY}rad) `; } // add transforms if (transforms.length) { styles += `transform:${transforms};`; } // add opacity if (isDefined(opacity)) { styles += `opacity:${opacity};`; // if we reach zero, we make the element inaccessible if (opacity === 0) { styles += `visibility:hidden;`; } // if we're below 100% opacity this element can't be clicked if (opacity < 1) { styles += `pointer-events:none;`; } } // add height if (isDefined(height)) { styles += `height:${height}px;`; } // add width if (isDefined(width)) { styles += `width:${width}px;`; } // apply styles const elementCurrentStyle = element.elementCurrentStyle || ''; // if new styles does not match current styles, lets update! if ( styles.length !== elementCurrentStyle.length || styles !== elementCurrentStyle ) { element.style.cssText = styles; // store current styles so we can compare them to new styles later on // _not_ getting the style value is faster element.elementCurrentStyle = styles; } }; ================================================ FILE: src/js/app/frame/mixins/utils/addGetSet.js ================================================ export const addGetSet = (keys, obj, props, overwrite = false) => { obj = Array.isArray(obj) ? obj : [obj]; obj.forEach(o => { keys.forEach(key => { let name = key; let getter = () => props[key]; let setter = value => (props[key] = value); if (typeof key === 'object') { name = key.key; getter = key.getter || getter; setter = key.setter || setter; } if (o[name] && !overwrite) { return; } o[name] = { get: getter, set: setter }; }); }); }; ================================================ FILE: src/js/app/frame/utils/AxisEnum.js ================================================ export const AxisEnum = { X: 0, Y: 1, Z: 2 }; ================================================ FILE: src/js/app/frame/utils/addEvent.js ================================================ export const addEvent = element => (type, fn) => { element.addEventListener(type, fn); }; ================================================ FILE: src/js/app/frame/utils/animators/easing.js ================================================ export const easeLinear = t => t; export const easeInOutQuad = t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t); ================================================ FILE: src/js/app/frame/utils/animators/spring.js ================================================ import { createObject } from '../../../../utils/createObject'; import { isNumber } from '../../../../utils/isNumber'; /** * Determines if position is at destination * @param position * @param destination * @param velocity * @param errorMargin * @returns {boolean} */ const thereYet = (position, destination, velocity, errorMargin = 0.001) => { return ( Math.abs(position - destination) < errorMargin && Math.abs(velocity) < errorMargin ); }; /** * Spring animation */ export const spring = // default options ({ stiffness = 0.5, damping = 0.75, mass = 10 } = {}) => // method definition { let target = null; let position = null; let velocity = 0; let resting = false; // updates spring state const interpolate = (ts, skipToEndState) => { // in rest, don't animate if (resting) return; // need at least a target or position to do springy things if (!(isNumber(target) && isNumber(position))) { resting = true; velocity = 0; return; } // calculate spring force const f = -(position - target) * stiffness; // update velocity by adding force based on mass velocity += f / mass; // update position by adding velocity position += velocity; // slow down based on amount of damping velocity *= damping; // we've arrived if we're near target and our velocity is near zero if (thereYet(position, target, velocity) || skipToEndState) { position = target; velocity = 0; resting = true; // we done api.onupdate(position); api.oncomplete(position); } else { // progress update api.onupdate(position); } }; /** * Set new target value * @param value */ const setTarget = value => { // if currently has no position, set target and position to this value if (isNumber(value) && !isNumber(position)) { position = value; } // next target value will not be animated to if (target === null) { target = value; position = value; } // let start moving to target target = value; // already at target if (position === target || typeof target === 'undefined') { // now resting as target is current position, stop moving resting = true; velocity = 0; // done! api.onupdate(position); api.oncomplete(position); return; } resting = false; }; // need 'api' to call onupdate callback const api = createObject({ interpolate, target: { set: setTarget, get: () => target }, resting: { get: () => resting }, onupdate: value => {}, oncomplete: value => {} }); return api; }; ================================================ FILE: src/js/app/frame/utils/animators/tween.js ================================================ import { createObject } from '../../../../utils/createObject'; import { easeInOutQuad } from './easing'; export const tween = // default values ({ duration = 500, easing = easeInOutQuad, delay = 0 } = {}) => // method definition { let start = null; let t; let p; let resting = true; let reverse = false; let target = null; const interpolate = (ts, skipToEndState) => { if (resting || target === null) return; if (start === null) { start = ts; } if (ts - start < delay) return; t = ts - start - delay; if (t >= duration || skipToEndState) { t = 1; p = reverse ? 0 : 1; api.onupdate(p * target); api.oncomplete(p * target); resting = true; } else { p = t / duration; api.onupdate( (t >= 0 ? easing(reverse ? 1 - p : p) : 0) * target ); } }; // need 'api' to call onupdate callback const api = createObject({ interpolate, target: { get: () => (reverse ? 0 : target), set: value => { // is initial value if (target === null) { target = value; api.onupdate(value); api.oncomplete(value); return; } // want to tween to a smaller value and have a current value if (value < target) { target = 1; reverse = true; } else { // not tweening to a smaller value reverse = false; target = value; } // let's go! resting = false; start = null; } }, resting: { get: () => resting }, onupdate: value => {}, oncomplete: value => {} }); return api; }; ================================================ FILE: src/js/app/frame/utils/appendChild.js ================================================ export const appendChild = parent => (child, index) => { if (typeof index !== 'undefined' && parent.children[index]) { parent.insertBefore(child, parent.children[index]); } else { parent.appendChild(child); } }; ================================================ FILE: src/js/app/frame/utils/appendChildView.js ================================================ export const appendChildView = (parent, childViews) => (view, index) => { if (typeof index !== 'undefined') { childViews.splice(index, 0, view); } else { childViews.push(view); } return view; }; ================================================ FILE: src/js/app/frame/utils/createAnimator.js ================================================ import { spring } from './animators/spring'; import { tween } from './animators/tween'; const animator = { spring, tween }; /* { type: 'spring', stiffness: .5, damping: .75, mass: 10 }; { translation: { type: 'spring', ... }, ... } { translation: { x: { type: 'spring', ... } } } */ export const createAnimator = (definition, category, property) => { // default is single definition // we check if transform is set, if so, we check if property is set const def = definition[category] && typeof definition[category][property] === 'object' ? definition[category][property] : definition[category] || definition; const type = typeof def === 'string' ? def : def.type; const props = typeof def === 'object' ? { ...def } : {}; return animator[type] ? animator[type](props) : null; }; ================================================ FILE: src/js/app/frame/utils/createElement.js ================================================ import { forin } from '../../../utils/forin'; import { attr } from '../../../utils/attr'; const ns = 'http://www.w3.org/2000/svg'; const svgElements = ['svg', 'path']; // only svg elements used const isSVGElement = tag => svgElements.includes(tag); export const createElement = (tag, className, attributes = {}) => { if (typeof className === 'object') { attributes = className; className = null; } const element = isSVGElement(tag) ? document.createElementNS(ns, tag) : document.createElement(tag); if (className) { if (isSVGElement(tag)) { attr(element, 'class', className); } else { element.className = className; } } forin(attributes, (name, value) => { attr(element, name, value); }); return element; }; ================================================ FILE: src/js/app/frame/utils/getChildCount.js ================================================ import { createElement } from './createElement'; import { isBrowser } from '../../../utils/isBrowser'; const testElement = isBrowser() ? createElement('svg') : {}; export const getChildCount = 'children' in testElement ? el => el.children.length : el => el.childNodes.length; ================================================ FILE: src/js/app/frame/utils/getViewRect.js ================================================ export const getViewRect = (elementRect, childViews, offset, scale) => { const left = offset[0] || elementRect.left; const top = offset[1] || elementRect.top; const right = left + elementRect.width; const bottom = top + elementRect.height * (scale[1] || 1); const rect = { // the rectangle of the element itself element: { ...elementRect }, // the rectangle of the element expanded to contain its children, does not include any margins inner: { left: elementRect.left, top: elementRect.top, right: elementRect.right, bottom: elementRect.bottom }, // the rectangle of the element expanded to contain its children including own margin and child margins // margins will be added after we've recalculated the size outer: { left, top, right, bottom } }; // expand rect to fit all child rectangles childViews .filter(childView => !childView.isRectIgnored()) .map(childView => childView.rect) .forEach(childViewRect => { expandRect(rect.inner, { ...childViewRect.inner }); expandRect(rect.outer, { ...childViewRect.outer }); }); // calculate inner width and height calculateRectSize(rect.inner); // append additional margin (top and left margins are included in top and left automatically) rect.outer.bottom += rect.element.marginBottom; rect.outer.right += rect.element.marginRight; // calculate outer width and height calculateRectSize(rect.outer); return rect; }; const expandRect = (parent, child) => { // adjust for parent offset child.top += parent.top; child.right += parent.left; child.bottom += parent.top; child.left += parent.left; if (child.bottom > parent.bottom) { parent.bottom = child.bottom; } if (child.right > parent.right) { parent.right = child.right; } }; const calculateRectSize = rect => { rect.width = rect.right - rect.left; rect.height = rect.bottom - rect.top; }; ================================================ FILE: src/js/app/frame/utils/removeChildView.js ================================================ export const removeChildView = (parent, childViews) => view => { // remove from child views childViews.splice(childViews.indexOf(view), 1); // remove the element if (view.element.parentNode) { parent.removeChild(view.element); } return view; }; ================================================ FILE: src/js/app/frame/utils/removeEvent.js ================================================ export const removeEvent = element => (type, fn) => { element.removeEventListener(type, fn); }; ================================================ FILE: src/js/app/frame/utils/updateRect.js ================================================ export const updateRect = (rect = {}, element = {}, style = {}) => { if (!element.layoutCalculated) { rect.paddingTop = parseInt(style.paddingTop, 10) || 0; rect.marginTop = parseInt(style.marginTop, 10) || 0; rect.marginRight = parseInt(style.marginRight, 10) || 0; rect.marginBottom = parseInt(style.marginBottom, 10) || 0; rect.marginLeft = parseInt(style.marginLeft, 10) || 0; element.layoutCalculated = true; } rect.left = element.offsetLeft || 0; rect.top = element.offsetTop || 0; rect.width = element.offsetWidth || 0; rect.height = element.offsetHeight || 0; rect.right = rect.left + rect.width; rect.bottom = rect.top + rect.height; rect.scrollTop = element.scrollTop; rect.hidden = element.offsetParent === null; return rect; }; ================================================ FILE: src/js/app/index.js ================================================ import { createStore } from './frame/index'; import { insertBefore } from '../utils/insertBefore'; import { insertAfter } from '../utils/insertAfter'; import { createInitialState } from './utils/createInitialState'; import { createObject } from '../utils/createObject'; import { createOptionAPI } from './utils/createOptionAPI'; import { createOptionActions } from './utils/createOptionActions'; import { createOptionQueries } from './utils/createOptionQueries'; import { InteractionMethod } from './enum/InteractionMethod'; import { getUniqueId } from '../utils/getUniqueId'; import { on } from './utils/on'; import { isArray } from '../utils/isArray'; import { isNumber } from '../utils/isNumber'; import { createItemAPI } from './utils/createItemAPI'; import { removeReleasedItems } from './utils/removeReleasedItems'; import { ItemStatus } from './enum/ItemStatus'; import { FileOrigin } from './enum/FileOrigin'; // defaults import { getOptions } from './options'; import { queries } from './queries'; import { actions } from './actions'; // view import { root } from './view/root'; // creates the app export const createApp = (initialOptions = {}) => { // let element let originalElement = null; // get default options const defaultOptions = getOptions(); // create the data store, this will contain all our app info const store = createStore( // initial state (should be serializable) createInitialState(defaultOptions), // queries [queries, createOptionQueries(defaultOptions)], // action handlers [actions, createOptionActions(defaultOptions)] ); // set initial options store.dispatch('SET_OPTIONS', { options: initialOptions }); // kick thread if visibility changes const visibilityHandler = () => { if (document.hidden) return; store.dispatch('KICK'); } document.addEventListener('visibilitychange', visibilityHandler); // re-render on window resize start and finish let resizeDoneTimer = null; let isResizing = false; let isResizingHorizontally = false; let initialWindowWidth = null; let currentWindowWidth = null; const resizeHandler = () => { if (!isResizing) { isResizing = true; } clearTimeout(resizeDoneTimer); resizeDoneTimer = setTimeout(() => { isResizing = false; initialWindowWidth = null; currentWindowWidth = null; if (isResizingHorizontally) { isResizingHorizontally = false; store.dispatch('DID_STOP_RESIZE'); } }, 500); }; window.addEventListener('resize', resizeHandler); // render initial view const view = root(store, { id: getUniqueId() }); // // PRIVATE API ------------------------------------------------------------------------------------- // let isResting = false; let isHidden = false; const readWriteApi = { // necessary for update loop /** * Reads from dom (never call manually) * @private */ _read: () => { // test if we're resizing horizontally // TODO: see if we can optimize this by measuring root rect if (isResizing) { currentWindowWidth = window.innerWidth; if (!initialWindowWidth) { initialWindowWidth = currentWindowWidth; } if (!isResizingHorizontally && currentWindowWidth !== initialWindowWidth) { store.dispatch('DID_START_RESIZE'); isResizingHorizontally = true; } } if (isHidden && isResting) { // test if is no longer hidden isResting = view.element.offsetParent === null; } // if resting, no need to read as numbers will still all be correct if (isResting) return; // read view data view._read(); // if is hidden we need to know so we exit rest mode when revealed isHidden = view.rect.element.hidden; }, /** * Writes to dom (never call manually) * @private */ _write: ts => { // get all actions from store const actions = store .processActionQueue() // filter out set actions (these will automatically trigger DID_SET) .filter(action => !/^SET_/.test(action.type)); // if was idling and no actions stop here if (isResting && !actions.length) return; // some actions might trigger events routeActionsToEvents(actions); // update the view isResting = view._write(ts, actions, isResizingHorizontally); // will clean up all archived items removeReleasedItems(store.query('GET_ITEMS')); // now idling if (isResting) { store.processDispatchQueue(); } } }; // // EXPOSE EVENTS ------------------------------------------------------------------------------------- // const createEvent = name => data => { // create default event const event = { type: name }; // no data to add if (!data) { return event; } // copy relevant props if (data.hasOwnProperty('error')) { event.error = data.error ? { ...data.error } : null; } if (data.status) { event.status = { ...data.status }; } if (data.file) { event.output = data.file; } // only source is available, else add item if possible if (data.source) { event.file = data.source; } else if (data.item || data.id) { const item = data.item ? data.item : store.query('GET_ITEM', data.id); event.file = item ? createItemAPI(item) : null; } // map all items in a possible items array if (data.items) { event.items = data.items.map(createItemAPI); } // if this is a progress event add the progress amount if (/progress/.test(name)) { event.progress = data.progress; } // copy relevant props if (data.hasOwnProperty('origin') && data.hasOwnProperty('target')) { event.origin = data.origin; event.target = data.target; } return event; }; const eventRoutes = { DID_DESTROY: createEvent('destroy'), DID_INIT: createEvent('init'), DID_THROW_MAX_FILES: createEvent('warning'), DID_INIT_ITEM: createEvent('initfile'), DID_START_ITEM_LOAD: createEvent('addfilestart'), DID_UPDATE_ITEM_LOAD_PROGRESS: createEvent('addfileprogress'), DID_LOAD_ITEM: createEvent('addfile'), DID_THROW_ITEM_INVALID: [ createEvent('error'), createEvent('addfile') ], DID_THROW_ITEM_LOAD_ERROR: [ createEvent('error'), createEvent('addfile') ], DID_THROW_ITEM_REMOVE_ERROR: [ createEvent('error'), createEvent('removefile') ], DID_PREPARE_OUTPUT: createEvent('preparefile'), DID_START_ITEM_PROCESSING: createEvent('processfilestart'), DID_UPDATE_ITEM_PROCESS_PROGRESS: createEvent('processfileprogress'), DID_ABORT_ITEM_PROCESSING: createEvent('processfileabort'), DID_COMPLETE_ITEM_PROCESSING: createEvent('processfile'), DID_COMPLETE_ITEM_PROCESSING_ALL: createEvent('processfiles'), DID_REVERT_ITEM_PROCESSING: createEvent('processfilerevert'), DID_THROW_ITEM_PROCESSING_ERROR: [ createEvent('error'), createEvent('processfile') ], DID_REMOVE_ITEM: createEvent('removefile'), DID_UPDATE_ITEMS: createEvent('updatefiles'), DID_ACTIVATE_ITEM: createEvent('activatefile'), DID_REORDER_ITEMS: createEvent('reorderfiles') }; const exposeEvent = event => { // create event object to be dispatched const detail = { pond: exports, ...event }; delete detail.type; view.element.dispatchEvent( new CustomEvent(`FilePond:${event.type}`, { // event info detail, // event behaviour bubbles: true, cancelable: true, composed: true // triggers listeners outside of shadow root }) ); // event object to params used for `on()` event handlers and callbacks `oninit()` const params = []; // if is possible error event, make it the first param if (event.hasOwnProperty('error')) { params.push(event.error); } // file is always section if (event.hasOwnProperty('file')) { params.push(event.file); } // append other props const filtered = ['type', 'error', 'file']; Object.keys(event) .filter(key => !filtered.includes(key)) .forEach(key => params.push(event[key])); // on(type, () => { }) exports.fire(event.type, ...params); // oninit = () => {} const handler = store.query(`GET_ON${event.type.toUpperCase()}`); if (handler) { handler(...params); } }; const routeActionsToEvents = actions => { if (!actions.length) return; actions .filter(action => eventRoutes[action.type]) .forEach(action => { const routes = eventRoutes[action.type]; (Array.isArray(routes) ? routes : [routes]).forEach(route => { // this isn't fantastic, but because of the stacking of settimeouts plugins can handle the did_load before the did_init if (action.type === 'DID_INIT_ITEM') { exposeEvent(route(action.data)); } else { setTimeout(() => { exposeEvent(route(action.data)); }, 0); } }); }); }; // // PUBLIC API ------------------------------------------------------------------------------------- // const setOptions = options => store.dispatch('SET_OPTIONS', { options }); const getFile = query => store.query('GET_ACTIVE_ITEM', query); const prepareFile = query => new Promise((resolve, reject) => { store.dispatch('REQUEST_ITEM_PREPARE', { query, success: (item) => { resolve(item) }, failure: (error) => { reject(error) }, }); }); const addFile = (source, options = {}) => new Promise((resolve, reject) => { addFiles([{source, options}], { index: options.index }) .then(items => resolve(items && items[0])) .catch(reject) }); const isFilePondFile = (obj) => obj.file && obj.id; const removeFile = (query, options) => { // if only passed options if (typeof query === 'object' && !isFilePondFile(query) && !options) { options = query; query = undefined; } // request item removal store.dispatch('REMOVE_ITEM', { ...options, query }); // see if item has been removed return store.query('GET_ACTIVE_ITEM', query) === null; }; const addFiles = (...args) => new Promise((resolve, reject) => { const sources = []; const options = {}; // user passed a sources array if (isArray(args[0])) { sources.push.apply(sources, args[0]); Object.assign(options, args[1] || {}); } else { // user passed sources as arguments, last one might be options object const lastArgument = args[args.length - 1]; if ( typeof lastArgument === 'object' && !(lastArgument instanceof Blob) ) { Object.assign(options, args.pop()); } // add rest to sources sources.push(...args); } store.dispatch('ADD_ITEMS', { items: sources, index: options.index, interactionMethod: InteractionMethod.API, success: resolve, failure: reject }); }); const getFiles = () => store.query('GET_ACTIVE_ITEMS'); const processFile = query => new Promise((resolve, reject) => { store.dispatch('REQUEST_ITEM_PROCESSING', { query, success: (item) => { resolve(item) }, failure: (error) => { reject(error) }, }); }); const prepareFiles = (...args) => { const queries = Array.isArray(args[0]) ? args[0] : args; const items = queries.length ? queries : getFiles(); return Promise.all(items.map(prepareFile)); }; const processFiles = (...args) => { const queries = Array.isArray(args[0]) ? args[0] : args; if (!queries.length) { const files = getFiles().filter(item => !(item.status === ItemStatus.IDLE && item.origin === FileOrigin.LOCAL) && item.status !== ItemStatus.PROCESSING && item.status !== ItemStatus.PROCESSING_COMPLETE && item.status !== ItemStatus.PROCESSING_REVERT_ERROR ); return Promise.all(files.map(processFile)); } return Promise.all(queries.map(processFile)); }; const removeFiles = (...args) => { const queries = Array.isArray(args[0]) ? args[0] : args; let options; if (typeof queries[queries.length - 1] === 'object') { options = queries.pop(); } else if (Array.isArray(args[0])) { options = args[1]; } const files = getFiles(); if (!queries.length) return Promise.all(files.map(file => removeFile(file, options))); // when removing by index the indexes shift after each file removal so we need to convert indexes to ids const mappedQueries = queries.map(query => isNumber(query) ? files[query] ? files[query].id : null : query ).filter(query => query); return mappedQueries.map(q => removeFile(q, options)); }; const exports = { // supports events ...on(), // inject private api methods ...readWriteApi, // inject all getters and setters ...createOptionAPI(store, defaultOptions), /** * Override options defined in options object * @param options */ setOptions, /** * Load the given file * @param source - the source of the file (either a File, base64 data uri or url) * @param options - object, { index: 0 } */ addFile, /** * Load the given files * @param sources - the sources of the files to load * @param options - object, { index: 0 } */ addFiles, /** * Returns the file objects matching the given query * @param query { string, number, null } */ getFile, /** * Upload file with given name * @param query { string, number, null } */ processFile, /** * Request prepare output for file with given name * @param query { string, number, null } */ prepareFile, /** * Removes a file by its name * @param query { string, number, null } */ removeFile, /** * Moves a file to a new location in the files list */ moveFile: (query, index) => store.dispatch('MOVE_ITEM', { query, index }), /** * Returns all files (wrapped in public api) */ getFiles, /** * Starts uploading all files */ processFiles, /** * Clears all files from the files list */ removeFiles, /** * Starts preparing output of all files */ prepareFiles, /** * Sort list of files */ sort: (compare) => store.dispatch('SORT', { compare }), /** * Browse the file system for a file */ browse: () => { // needs to be trigger directly as user action needs to be traceable (is not traceable in requestAnimationFrame) var input = view.element.querySelector('input[type=file]'); if (input) { input.click(); } }, /** * Destroys the app */ destroy: () => { // request destruction exports.fire('destroy', view.element); // stop active processes (file uploads, fetches, stuff like that) // loop over items and depending on states call abort for ongoing processes store.dispatch('ABORT_ALL'); // destroy view view._destroy(); // stop listening to resize window.removeEventListener('resize', resizeHandler); // stop listening to the visiblitychange event document.removeEventListener('visibilitychange', visibilityHandler); // dispatch destroy store.dispatch('DID_DESTROY'); }, /** * Inserts the plugin before the target element */ insertBefore: element => insertBefore(view.element, element), /** * Inserts the plugin after the target element */ insertAfter: element => insertAfter(view.element, element), /** * Appends the plugin to the target element */ appendTo: element => element.appendChild(view.element), /** * Replaces an element with the app */ replaceElement: element => { // insert the app before the element insertBefore(view.element, element); // remove the original element element.parentNode.removeChild(element); // remember original element originalElement = element; }, /** * Restores the original element */ restoreElement: () => { if (!originalElement) { return; // no element to restore } // restore original element insertAfter(originalElement, view.element); // remove our element view.element.parentNode.removeChild(view.element); // remove reference originalElement = null; }, /** * Returns true if the app root is attached to given element * @param element */ isAttachedTo: element => view.element === element || originalElement === element, /** * Returns the root element */ element: { get: () => view.element }, /** * Returns the current pond status */ status: { get: () => store.query('GET_STATUS') } }; // Done! store.dispatch('DID_INIT'); // create actual api object return createObject(exports); }; ================================================ FILE: src/js/app/options.js ================================================ import { getDecimalSeparator } from '../utils/getDecimalSeparator'; import { getThousandsSeparator } from '../utils/getThousandsSeparator'; import { Type } from './enum/Type'; import { applyFilters } from './../filter'; import { forin } from '../utils/forin'; import { isString } from '../utils/isString'; import { createServerAPI } from './utils/createServerAPI'; import { getValueByType } from './utils/getValueByType'; export const extendDefaultOptions = additionalOptions => Object.assign(defaultOptions, additionalOptions); export const getOptions = () => ({ ...defaultOptions }); export const setOptions = opts => { forin(opts, (key, value) => { // key does not exist, so this option cannot be set if (!defaultOptions[key]) { return; } defaultOptions[key][0] = getValueByType( value, defaultOptions[key][0], defaultOptions[key][1] ); }); }; // default options on app export const defaultOptions = { // the id to add to the root element id: [null, Type.STRING], // input field name to use name: ['filepond', Type.STRING], // disable the field disabled: [false, Type.BOOLEAN], // classname to put on wrapper className: [null, Type.STRING], // is the field required required: [false, Type.BOOLEAN], // Allow media capture when value is set captureMethod: [null, Type.STRING], // - "camera", "microphone" or "camcorder", // - Does not work with multiple on apple devices // - If set, acceptedFileTypes must be made to match with media wildcard "image/*", "audio/*" or "video/*" // sync `acceptedFileTypes` property with `accept` attribute allowSyncAcceptAttribute: [true, Type.BOOLEAN], // Feature toggles allowDrop: [true, Type.BOOLEAN], // Allow dropping of files allowBrowse: [true, Type.BOOLEAN], // Allow browsing the file system allowPaste: [true, Type.BOOLEAN], // Allow pasting files allowMultiple: [false, Type.BOOLEAN], // Allow multiple files (disabled by default, as multiple attribute is also required on input to allow multiple) allowReplace: [true, Type.BOOLEAN], // Allow dropping a file on other file to replace it (only works when multiple is set to false) allowRevert: [true, Type.BOOLEAN], // Allows user to revert file upload allowRemove: [true, Type.BOOLEAN], // Allow user to remove a file allowProcess: [true, Type.BOOLEAN], // Allows user to process a file, when set to false, this removes the file upload button allowReorder: [false, Type.BOOLEAN], // Allow reordering of files allowDirectoriesOnly: [false, Type.BOOLEAN], // Allow only selecting directories with browse (no support for filtering dnd at this point) // Try store file if `server` not set storeAsFile: [false, Type.BOOLEAN], // Revert mode forceRevert: [false, Type.BOOLEAN], // Set to 'force' to require the file to be reverted before removal // Input requirements maxFiles: [null, Type.INT], // Max number of files checkValidity: [false, Type.BOOLEAN], // Enables custom validity messages // Where to put file itemInsertLocationFreedom: [true, Type.BOOLEAN], // Set to false to always add items to begin or end of list itemInsertLocation: ['before', Type.STRING], // Default index in list to add items that have been dropped at the top of the list itemInsertInterval: [75, Type.INT], // Drag 'n Drop related dropOnPage: [false, Type.BOOLEAN], // Allow dropping of files anywhere on page (prevents browser from opening file if dropped outside of Up) dropOnElement: [true, Type.BOOLEAN], // Drop needs to happen on element (set to false to also load drops outside of Up) dropValidation: [false, Type.BOOLEAN], // Enable or disable validating files on drop ignoredFiles: [['.ds_store', 'thumbs.db', 'desktop.ini'], Type.ARRAY], // Upload related instantUpload: [true, Type.BOOLEAN], // Should upload files immediately on drop maxParallelUploads: [2, Type.INT], // Maximum files to upload in parallel allowMinimumUploadDuration: [true, Type.BOOLEAN], // if true uploads take at least 750 ms, this ensures the user sees the upload progress giving trust the upload actually happened // Chunks chunkUploads: [false, Type.BOOLEAN], // Enable chunked uploads chunkForce: [false, Type.BOOLEAN], // Force use of chunk uploads even for files smaller than chunk size chunkSize: [5000000, Type.INT], // Size of chunks (5MB default) chunkRetryDelays: [[500, 1000, 3000], Type.ARRAY], // Amount of times to retry upload of a chunk when it fails // The server api end points to use for uploading (see docs) server: [null, Type.SERVER_API], // File size calculations, can set to 1024, this is only used for display, properties use file size base 1000 fileSizeBase: [1000, Type.INT], // Labels and status messages labelFileSizeBytes: ['bytes', Type.STRING], labelFileSizeKilobytes: ['KB', Type.STRING], labelFileSizeMegabytes: ['MB', Type.STRING], labelFileSizeGigabytes: ['GB', Type.STRING], labelDecimalSeparator: [getDecimalSeparator(), Type.STRING], // Default is locale separator labelThousandsSeparator: [getThousandsSeparator(), Type.STRING], // Default is locale separator labelIdle: [ 'Drag & Drop your files or Browse', Type.STRING, ], labelInvalidField: ['Field contains invalid files', Type.STRING], labelFileWaitingForSize: ['Waiting for size', Type.STRING], labelFileSizeNotAvailable: ['Size not available', Type.STRING], labelFileCountSingular: ['file in list', Type.STRING], labelFileCountPlural: ['files in list', Type.STRING], labelFileLoading: ['Loading', Type.STRING], labelFileAdded: ['Added', Type.STRING], // assistive only labelFileLoadError: ['Error during load', Type.STRING], labelFileRemoved: ['Removed', Type.STRING], // assistive only labelFileRemoveError: ['Error during remove', Type.STRING], labelFileProcessing: ['Uploading', Type.STRING], labelFileProcessingComplete: ['Upload complete', Type.STRING], labelFileProcessingAborted: ['Upload cancelled', Type.STRING], labelFileProcessingError: ['Error during upload', Type.STRING], labelFileProcessingRevertError: ['Error during revert', Type.STRING], labelTapToCancel: ['tap to cancel', Type.STRING], labelTapToRetry: ['tap to retry', Type.STRING], labelTapToUndo: ['tap to undo', Type.STRING], labelButtonRemoveItem: ['Remove', Type.STRING], labelButtonAbortItemLoad: ['Abort', Type.STRING], labelButtonRetryItemLoad: ['Retry', Type.STRING], labelButtonAbortItemProcessing: ['Cancel', Type.STRING], labelButtonUndoItemProcessing: ['Undo', Type.STRING], labelButtonRetryItemProcessing: ['Retry', Type.STRING], labelButtonProcessItem: ['Upload', Type.STRING], // make sure width and height plus viewpox are even numbers so icons are nicely centered iconRemove: [ '', Type.STRING, ], iconProcess: [ '', Type.STRING, ], iconRetry: [ '', Type.STRING, ], iconUndo: [ '', Type.STRING, ], iconDone: [ '', Type.STRING, ], // event handlers oninit: [null, Type.FUNCTION], onwarning: [null, Type.FUNCTION], onerror: [null, Type.FUNCTION], onactivatefile: [null, Type.FUNCTION], oninitfile: [null, Type.FUNCTION], onaddfilestart: [null, Type.FUNCTION], onaddfileprogress: [null, Type.FUNCTION], onaddfile: [null, Type.FUNCTION], onprocessfilestart: [null, Type.FUNCTION], onprocessfileprogress: [null, Type.FUNCTION], onprocessfileabort: [null, Type.FUNCTION], onprocessfilerevert: [null, Type.FUNCTION], onprocessfile: [null, Type.FUNCTION], onprocessfiles: [null, Type.FUNCTION], onremovefile: [null, Type.FUNCTION], onpreparefile: [null, Type.FUNCTION], onupdatefiles: [null, Type.FUNCTION], onreorderfiles: [null, Type.FUNCTION], // hooks beforeDropFile: [null, Type.FUNCTION], beforeAddFile: [null, Type.FUNCTION], beforeRemoveFile: [null, Type.FUNCTION], beforePrepareFile: [null, Type.FUNCTION], // styles stylePanelLayout: [null, Type.STRING], // null 'integrated', 'compact', 'circle' stylePanelAspectRatio: [null, Type.STRING], // null or '3:2' or 1 styleItemPanelAspectRatio: [null, Type.STRING], styleButtonRemoveItemPosition: ['left', Type.STRING], styleButtonProcessItemPosition: ['right', Type.STRING], styleLoadIndicatorPosition: ['right', Type.STRING], styleProgressIndicatorPosition: ['right', Type.STRING], styleButtonRemoveItemAlign: [false, Type.BOOLEAN], // custom initial files array files: [[], Type.ARRAY], // show support by displaying credits credits: [['https://filepond.com', 'Powered by FilePond'], Type.ARRAY], }; ================================================ FILE: src/js/app/queries.js ================================================ import { isObject } from '../utils/isObject'; import { isFunction } from '../utils/isFunction'; import { getItemByQuery } from './utils/getItemByQuery'; import { getNumericAspectRatioFromString } from '../utils/getNumericAspectRatioFromString'; import { getActiveItems } from './utils/getActiveItems'; import { Status } from './enum/Status'; import { ItemStatus } from './enum/ItemStatus'; import { canUpdateFileInput } from '../utils/canUpdateFileInput'; const ITEM_ERROR = [ ItemStatus.LOAD_ERROR, ItemStatus.PROCESSING_ERROR, ItemStatus.PROCESSING_REVERT_ERROR, ]; const ITEM_BUSY = [ ItemStatus.LOADING, ItemStatus.PROCESSING, ItemStatus.PROCESSING_QUEUED, ItemStatus.INIT, ]; const ITEM_READY = [ItemStatus.PROCESSING_COMPLETE]; const isItemInErrorState = item => ITEM_ERROR.includes(item.status); const isItemInBusyState = item => ITEM_BUSY.includes(item.status); const isItemInReadyState = item => ITEM_READY.includes(item.status); const isAsync = state => isObject(state.options.server) && (isObject(state.options.server.process) || isFunction(state.options.server.process)); export const queries = state => ({ GET_STATUS: () => { const items = getActiveItems(state.items); const { EMPTY, ERROR, BUSY, IDLE, READY } = Status; if (items.length === 0) return EMPTY; if (items.some(isItemInErrorState)) return ERROR; if (items.some(isItemInBusyState)) return BUSY; if (items.some(isItemInReadyState)) return READY; return IDLE; }, GET_ITEM: query => getItemByQuery(state.items, query), GET_ACTIVE_ITEM: query => getItemByQuery(getActiveItems(state.items), query), GET_ACTIVE_ITEMS: () => getActiveItems(state.items), GET_ITEMS: () => state.items, GET_ITEM_NAME: query => { const item = getItemByQuery(state.items, query); return item ? item.filename : null; }, GET_ITEM_SIZE: query => { const item = getItemByQuery(state.items, query); return item ? item.fileSize : null; }, GET_STYLES: () => Object.keys(state.options) .filter(key => /^style/.test(key)) .map(option => ({ name: option, value: state.options[option], })), GET_PANEL_ASPECT_RATIO: () => { const isShapeCircle = /circle/.test(state.options.stylePanelLayout); const aspectRatio = isShapeCircle ? 1 : getNumericAspectRatioFromString(state.options.stylePanelAspectRatio); return aspectRatio; }, GET_ITEM_PANEL_ASPECT_RATIO: () => state.options.styleItemPanelAspectRatio, GET_ITEMS_BY_STATUS: status => getActiveItems(state.items).filter(item => item.status === status), GET_TOTAL_ITEMS: () => getActiveItems(state.items).length, SHOULD_UPDATE_FILE_INPUT: () => state.options.storeAsFile && canUpdateFileInput() && !isAsync(state), IS_ASYNC: () => isAsync(state), GET_FILE_SIZE_LABELS: query => ({ labelBytes: query('GET_LABEL_FILE_SIZE_BYTES') || undefined, labelKilobytes: query('GET_LABEL_FILE_SIZE_KILOBYTES') || undefined, labelMegabytes: query('GET_LABEL_FILE_SIZE_MEGABYTES') || undefined, labelGigabytes: query('GET_LABEL_FILE_SIZE_GIGABYTES') || undefined, }), }); ================================================ FILE: src/js/app/utils/buildURL.js ================================================ const hasQS = str => /\?/.test(str); export const buildURL = (...parts) => { let url = ''; parts.forEach(part => { url += hasQS(url) && hasQS(part) ? part.replace(/\?/, '&') : part; }); return url; } ================================================ FILE: src/js/app/utils/convertTo.js ================================================ import { toArray } from '../../utils/toArray'; import { toBoolean } from '../../utils/toBoolean'; import { toInt } from '../../utils/toInt'; import { toFloat } from '../../utils/toFloat'; import { toBytes } from '../../utils/toBytes'; import { toString } from '../../utils/toString'; import { isFunction } from '../../utils/isFunction'; import { toFunctionReference } from '../../utils/toFunctionReference'; import { toServerAPI } from './toServerAPI'; import { getType } from './getType'; const replaceSingleQuotes = (str) => str .replace(/{\s*'/g,'{"') .replace(/'\s*}/g,'"}') .replace(/'\s*:/g,'":') .replace(/:\s*'/g,':"') .replace(/,\s*'/g,',"') .replace(/'\s*,/g,'",') const conversionTable = { array: toArray, boolean: toBoolean, int: value => getType(value) === 'bytes' ? toBytes(value) : toInt(value), number: toFloat, float: toFloat, bytes: toBytes, string: value => isFunction(value) ? value : toString(value), function: value => toFunctionReference(value), serverapi: toServerAPI, object: value => { try { return JSON.parse(replaceSingleQuotes(value)) } catch(e) { return null; } } }; export const convertTo = (value, type) => conversionTable[type](value); ================================================ FILE: src/js/app/utils/createDragHelper.js ================================================ export const createDragHelper = (items) => { const itemIds = items.map(item => item.id); let prevIndex = undefined; return { setIndex: (index) => { prevIndex = index; }, getIndex: () => prevIndex, getItemIndex: (item) => itemIds.indexOf(item.id) } } ================================================ FILE: src/js/app/utils/createFetchFunction.js ================================================ import { sendRequest } from '../../utils/sendRequest'; import { createResponse } from '../../utils/createResponse'; import { createTimeoutResponse } from '../../utils/createDefaultResponse'; import { getFileInfoFromHeaders } from '../../utils/getFileInfoFromHeaders'; import { getFilenameFromURL } from '../../utils/getFilenameFromURL'; import { getFileFromBlob } from '../../utils/getFileFromBlob'; import { isString } from '../../utils/isString'; import { buildURL } from './buildURL'; export const createFetchFunction = (apiUrl = '', action) => { // custom handler (should also handle file, load, error, progress and abort) if (typeof action === 'function') { return action; } // no action supplied if (!action || !isString(action.url)) { return null; } // set onload hanlder const onload = action.onload || (res => res); const onerror = action.onerror || (res => null); // internal handler return (url, load, error, progress, abort, headers) => { // do local or remote request based on if the url is external const request = sendRequest(url, buildURL(apiUrl, action.url), { ...action, responseType: 'blob' }); request.onload = (xhr) => { // get headers const headers = xhr.getAllResponseHeaders(); // get filename const filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url); // create response load( createResponse( 'load', xhr.status, action.method === 'HEAD' ? null : getFileFromBlob(onload(xhr.response), filename), headers ) ) }; request.onerror = (xhr) => { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.onheaders = (xhr) => { headers( createResponse( 'headers', xhr.status, null, xhr.getAllResponseHeaders() ) ) } request.ontimeout = createTimeoutResponse(error); request.onprogress = progress; request.onabort = abort; // should return request return request; }; }; ================================================ FILE: src/js/app/utils/createFileLoader.js ================================================ import { isBase64DataURI } from '../../utils/isBase64DataURI'; import { getFilenameFromURL } from '../../utils/getFilenameFromURL'; import { getFileFromBase64DataURI } from '../../utils/getFileFromBase64DataURI'; import { getFileFromBlob } from '../../utils/getFileFromBlob'; import { getFileInfoFromHeaders } from '../../utils/getFileInfoFromHeaders'; import { on } from '../utils/on'; export const createFileLoader = fetchFn => { const state = { source: null, complete: false, progress: 0, size: null, timestamp: null, duration: 0, request: null }; const getProgress = () => state.progress; const abort = () => { if (state.request && state.request.abort) { state.request.abort(); } }; // load source const load = () => { // get quick reference const source = state.source; api.fire('init', source); // Load Files if (source instanceof File) { api.fire('load', source); } else if (source instanceof Blob) { // Load blobs, set default name to current date api.fire('load', getFileFromBlob(source, source.name)); } else if (isBase64DataURI(source)) { // Load base 64, set default name to current date api.fire('load', getFileFromBase64DataURI(source)); } else { // Deal as if is external URL, let's load it! loadURL(source); } }; // loads a url const loadURL = url => { // is remote url and no fetch method supplied if (!fetchFn) { api.fire('error', { type: 'error', body: 'Can\'t load URL', code: 400 }); return; } // set request start state.timestamp = Date.now(); // load file state.request = fetchFn( url, response => { // update duration state.duration = Date.now() - state.timestamp; // done! state.complete = true; // turn blob response into a file if (response instanceof Blob) { response = getFileFromBlob( response, response.name || getFilenameFromURL(url) ); } api.fire( 'load', // if has received blob, we go with blob, if no response, we return null response instanceof Blob ? response : response ? response.body : null ); }, error => { api.fire( 'error', typeof error === 'string' ? { type: 'error', code: 0, body: error } : error ); }, (computable, current, total) => { // collected some meta data already if (total) { state.size = total; } // update duration state.duration = Date.now() - state.timestamp; // if we can't compute progress, we're not going to fire progress events if (!computable) { state.progress = null; return; } // update progress percentage state.progress = current / total; // expose api.fire('progress', state.progress); }, () => { api.fire('abort'); }, response => { const fileinfo = getFileInfoFromHeaders(typeof response === 'string' ? response : response.headers); api.fire('meta', { size: state.size || fileinfo.size, filename: fileinfo.name, source: fileinfo.source }); } ); }; const api = { ...on(), setSource: source => state.source = source, getProgress, // file load progress abort, // abort file load load // start load }; return api; }; ================================================ FILE: src/js/app/utils/createFileProcessor.js ================================================ import { createPerceivedPerformanceUpdater } from './createPerceivedPerformanceUpdater'; import { getRandomNumber } from '../../utils/getRandomNumber'; import { on } from '../utils/on'; import { isObject } from '../../utils/isObject'; export const createFileProcessor = (processFn, options) => { const state = { complete: false, perceivedProgress: 0, perceivedPerformanceUpdater: null, progress: null, timestamp: null, perceivedDuration: 0, duration: 0, request: null, response: null, }; const { allowMinimumUploadDuration } = options; const process = (file, metadata) => { const progressFn = () => { // we've not yet started the real download, stop here // the request might not go through, for instance, there might be some server trouble // if state.progress is null, the server does not allow computing progress and we show the spinner instead if (state.duration === 0 || state.progress === null) return; // as we're now processing, fire the progress event api.fire('progress', api.getProgress()); }; const completeFn = () => { state.complete = true; api.fire('load-perceived', state.response.body); }; // let's start processing api.fire('start'); // set request start state.timestamp = Date.now(); // create perceived performance progress indicator state.perceivedPerformanceUpdater = createPerceivedPerformanceUpdater( progress => { state.perceivedProgress = progress; state.perceivedDuration = Date.now() - state.timestamp; progressFn(); // if fake progress is done, and a response has been received, // and we've not yet called the complete method if (state.response && state.perceivedProgress === 1 && !state.complete) { // we done! completeFn(); } }, // random delay as in a list of files you start noticing // files uploading at the exact same speed allowMinimumUploadDuration ? getRandomNumber(750, 1500) : 0 ); // remember request so we can abort it later state.request = processFn( // the file to process file, // the metadata to send along metadata, // callbacks (load, error, progress, abort, transfer) // load expects the body to be a server id if // you want to make use of revert response => { // we put the response in state so we can access // it outside of this method state.response = isObject(response) ? response : { type: 'load', code: 200, body: `${response}`, headers: {}, }; // update duration state.duration = Date.now() - state.timestamp; // force progress to 1 as we're now done state.progress = 1; // actual load is done let's share results api.fire('load', state.response.body); // we are really done // if perceived progress is 1 ( wait for perceived progress to complete ) // or if server does not support progress ( null ) if ( !allowMinimumUploadDuration || (allowMinimumUploadDuration && state.perceivedProgress === 1) ) { completeFn(); } }, // error is expected to be an object with type, code, body error => { // cancel updater state.perceivedPerformanceUpdater.clear(); // update others about this error api.fire( 'error', isObject(error) ? error : { type: 'error', code: 0, body: `${error}`, } ); }, // actual processing progress (computable, current, total) => { // update actual duration state.duration = Date.now() - state.timestamp; // update actual progress state.progress = computable ? current / total : null; progressFn(); }, // abort does not expect a value () => { // stop updater state.perceivedPerformanceUpdater.clear(); // fire the abort event so we can switch visuals api.fire('abort', state.response ? state.response.body : null); }, // register the id for this transfer transferId => { api.fire('transfer', transferId); } ); }; const abort = () => { // no request running, can't abort if (!state.request) return; // stop updater state.perceivedPerformanceUpdater.clear(); // abort actual request if (state.request.abort) state.request.abort(); // if has response object, we've completed the request state.complete = true; }; const reset = () => { abort(); state.complete = false; state.perceivedProgress = 0; state.progress = 0; state.timestamp = null; state.perceivedDuration = 0; state.duration = 0; state.request = null; state.response = null; }; const getProgress = allowMinimumUploadDuration ? () => (state.progress ? Math.min(state.progress, state.perceivedProgress) : null) : () => state.progress || null; const getDuration = allowMinimumUploadDuration ? () => Math.min(state.duration, state.perceivedDuration) : () => state.duration; const api = { ...on(), process, // start processing file abort, // abort active process request getProgress, getDuration, reset, }; return api; }; ================================================ FILE: src/js/app/utils/createFileProcessorFunction.js ================================================ import { sendRequest } from '../../utils/sendRequest'; import { createResponse } from '../../utils/createResponse'; import { createTimeoutResponse } from '../../utils/createDefaultResponse'; import { isObject } from '../../utils/isObject'; import { buildURL } from './buildURL'; import { processFileChunked } from './processFileChunked'; /* function signature: (file, metadata, load, error, progress, abort) => { return { abort:() => {} } } */ export const createFileProcessorFunction = (apiUrl, action, name, options) => ( file, metadata, load, error, progress, abort, transfer ) => { // no file received if (!file) return; // if was passed a file, and we can chunk it, exit here const canChunkUpload = options.chunkUploads; const shouldChunkUpload = canChunkUpload && file.size > options.chunkSize; const willChunkUpload = canChunkUpload && (shouldChunkUpload || options.chunkForce); if (file instanceof Blob && willChunkUpload) return processFileChunked( apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options ); // set handlers const ondata = action.ondata || (fd => fd); const onload = action.onload || (res => res); const onerror = action.onerror || (res => null); const headers = typeof action.headers === 'function' ? action.headers(file, metadata) || {} : { ...action.headers, }; const requestParams = { ...action, headers, }; // create formdata object var formData = new FormData(); // add metadata under same name if (isObject(metadata)) { formData.append(name, JSON.stringify(metadata)); } // Turn into an array of objects so no matter what the input, we can handle it the same way (file instanceof Blob ? [{ name: null, file }] : file).forEach(item => { formData.append( name, item.file, item.name === null ? item.file.name : `${item.name}${item.file.name}` ); }); // send request object const request = sendRequest(ondata(formData), buildURL(apiUrl, action.url), requestParams); request.onload = xhr => { load(createResponse('load', xhr.status, onload(xhr.response), xhr.getAllResponseHeaders())); }; request.onerror = xhr => { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.ontimeout = createTimeoutResponse(error); request.onprogress = progress; request.onabort = abort; // should return request return request; }; ================================================ FILE: src/js/app/utils/createFileStub.js ================================================ import { getMimeTypeFromBase64DataURI } from '../../utils/getMimeTypeFromBase64DataURI'; import { getFilenameFromURL } from '../../utils/getFilenameFromURL'; import { isBase64DataURI } from '../../utils/isBase64DataURI'; import { isString } from '../../utils/isString'; import { getDateString } from '../../utils/getDateString'; export const createFileStub = source => { let data = [source.name, source.size, source.type]; // is blob or base64, then we need to set the name if (source instanceof Blob || isBase64DataURI(source)) { data[0] = source.name || getDateString(); } else if (isBase64DataURI(source)) { // if is base64 data uri we need to determine the average size and type data[1] = source.length; data[2] = getMimeTypeFromBase64DataURI(source); } else if (isString(source)) { // url data[0] = getFilenameFromURL(source); data[1] = 0; data[2] = 'application/octet-stream'; } return { name: data[0], size: data[1], type: data[2] }; }; ================================================ FILE: src/js/app/utils/createHopper.js ================================================ import { createDragNDropClient } from '../utils/dnd'; export const createHopper = (scope, validateItems, options) => { // is now hopper scope scope.classList.add('filepond--hopper'); // shortcuts const { catchesDropsOnPage, requiresDropOnElement, filterItems = items => items } = options; // create a dnd client const client = createDragNDropClient( scope, catchesDropsOnPage ? document.documentElement : scope, requiresDropOnElement ); // current client state let lastState = ''; let currentState = ''; // determines if a file may be dropped client.allowdrop = items => { // TODO: if we can, throw error to indicate the items cannot by dropped return validateItems(filterItems(items)); }; client.ondrop = (position, items) => { const filteredItems = filterItems(items); if (!validateItems(filteredItems)) { api.ondragend(position); return; } currentState = 'drag-drop'; api.onload(filteredItems, position); }; client.ondrag = position => { api.ondrag(position); }; client.onenter = position => { currentState = 'drag-over'; api.ondragstart(position); }; client.onexit = position => { currentState = 'drag-exit'; api.ondragend(position); }; const api = { updateHopperState: () => { if (lastState !== currentState) { scope.dataset.hopperState = currentState; lastState = currentState; } }, onload: () => {}, ondragstart: () => {}, ondrag: () => {}, ondragend: () => {}, destroy: () => { // destroy client client.destroy(); } }; return api; }; ================================================ FILE: src/js/app/utils/createInitialState.js ================================================ import { createOptions } from './createOptions'; export const createInitialState = options => ({ // model items: [], // timeout used for calling update items listUpdateTimeout: null, // timeout used for stacking metadata updates itemUpdateTimeout: null, // queue of items waiting to be processed processingQueue: [], // options options: createOptions(options) }); ================================================ FILE: src/js/app/utils/createItem.js ================================================ import { getUniqueId } from '../../utils/getUniqueId'; import { getFilenameWithoutExtension } from '../../utils/getFilenameWithoutExtension'; import { getExtensionFromFilename } from '../../utils/getExtensionFromFilename'; import { ItemStatus } from '../enum/ItemStatus'; import { on } from './on'; import { createFileStub } from './createFileStub'; import { createObject } from '../../utils/createObject'; import { FileOrigin } from '../../app/enum/FileOrigin'; import { isObject } from '../../utils/isObject'; import { isFile } from '../../utils/isFile'; import { deepCloneObject } from '../../utils/deepCloneObject'; export const createItem = (origin = null, serverFileReference = null, file = null) => { // unique id for this item, is used to identify the item across views const id = getUniqueId(); /** * Internal item state */ const state = { // is archived archived: false, // if is frozen, no longer fires events frozen: false, // removed from view released: false, // original source source: null, // file model reference file, // id of file on server serverFileReference, // id of file transfer on server transferId: null, // is aborted processingAborted: false, // current item status status: serverFileReference ? ItemStatus.PROCESSING_COMPLETE : ItemStatus.INIT, // active processes activeLoader: null, activeProcessor: null, }; // callback used when abort processing is called to link back to the resolve method let abortProcessingRequestComplete = null; /** * Externally added item metadata */ const metadata = {}; // item data const setStatus = status => (state.status = status); // fire event unless the item has been archived const fire = (event, ...params) => { if (state.released || state.frozen) return; api.fire(event, ...params); }; // file data const getFileExtension = () => getExtensionFromFilename(state.file.name); const getFileType = () => state.file.type; const getFileSize = () => state.file.size; const getFile = () => state.file; // // logic to load a file // const load = (source, loader, onload) => { // remember the original item source state.source = source; // source is known api.fireSync('init'); // file stub is already there if (state.file) { api.fireSync('load-skip'); return; } // set a stub file object while loading the actual data state.file = createFileStub(source); // starts loading loader.on('init', () => { fire('load-init'); }); // we'eve received a size indication, let's update the stub loader.on('meta', meta => { // set size of file stub state.file.size = meta.size; // set name of file stub state.file.filename = meta.filename; // if has received source, we done if (meta.source) { origin = FileOrigin.LIMBO; state.serverFileReference = meta.source; state.status = ItemStatus.PROCESSING_COMPLETE; } // size has been updated fire('load-meta'); }); // the file is now loading we need to update the progress indicators loader.on('progress', progress => { setStatus(ItemStatus.LOADING); fire('load-progress', progress); }); // an error was thrown while loading the file, we need to switch to error state loader.on('error', error => { setStatus(ItemStatus.LOAD_ERROR); fire('load-request-error', error); }); // user or another process aborted the file load (cannot retry) loader.on('abort', () => { setStatus(ItemStatus.INIT); fire('load-abort'); }); // done loading loader.on('load', file => { // as we've now loaded the file the loader is no longer required state.activeLoader = null; // called when file has loaded succesfully const success = result => { // set (possibly) transformed file state.file = isFile(result) ? result : state.file; // file received if (origin === FileOrigin.LIMBO && state.serverFileReference) { setStatus(ItemStatus.PROCESSING_COMPLETE); } else { setStatus(ItemStatus.IDLE); } fire('load'); }; const error = result => { // set original file state.file = file; fire('load-meta'); setStatus(ItemStatus.LOAD_ERROR); fire('load-file-error', result); }; // if we already have a server file reference, we don't need to call the onload method if (state.serverFileReference) { success(file); return; } // no server id, let's give this file the full treatment onload(file, success, error); }); // set loader source data loader.setSource(source); // set as active loader state.activeLoader = loader; // load the source data loader.load(); }; const retryLoad = () => { if (!state.activeLoader) { return; } state.activeLoader.load(); }; const abortLoad = () => { if (state.activeLoader) { state.activeLoader.abort(); return; } setStatus(ItemStatus.INIT); fire('load-abort'); }; // // logic to process a file // const process = (processor, onprocess) => { // processing was aborted if (state.processingAborted) { state.processingAborted = false; return; } // now processing setStatus(ItemStatus.PROCESSING); // reset abort callback abortProcessingRequestComplete = null; // if no file loaded we'll wait for the load event if (!(state.file instanceof Blob)) { api.on('load', () => { process(processor, onprocess); }); return; } // setup processor processor.on('load', serverFileReference => { // need this id to be able to revert the upload state.transferId = null; state.serverFileReference = serverFileReference; }); // register transfer id processor.on('transfer', transferId => { // need this id to be able to revert the upload state.transferId = transferId; }); processor.on('load-perceived', serverFileReference => { // no longer required state.activeProcessor = null; // need this id to be able to rever the upload state.transferId = null; state.serverFileReference = serverFileReference; setStatus(ItemStatus.PROCESSING_COMPLETE); fire('process-complete', serverFileReference); }); processor.on('start', () => { fire('process-start'); }); processor.on('error', error => { state.activeProcessor = null; setStatus(ItemStatus.PROCESSING_ERROR); fire('process-error', error); }); processor.on('abort', serverFileReference => { state.activeProcessor = null; // if file was uploaded but processing was cancelled during perceived processor time store file reference state.serverFileReference = serverFileReference; setStatus(ItemStatus.IDLE); fire('process-abort'); // has timeout so doesn't interfere with remove action if (abortProcessingRequestComplete) { abortProcessingRequestComplete(); } }); processor.on('progress', progress => { fire('process-progress', progress); }); // when successfully transformed const success = file => { // if was archived in the mean time, don't process if (state.archived) return; // process file! processor.process(file, { ...metadata }); }; // something went wrong during transform phase const error = console.error; // start processing the file onprocess(state.file, success, error); // set as active processor state.activeProcessor = processor; }; const requestProcessing = () => { state.processingAborted = false; setStatus(ItemStatus.PROCESSING_QUEUED); }; const abortProcessing = () => new Promise(resolve => { if (!state.activeProcessor) { state.processingAborted = true; setStatus(ItemStatus.IDLE); fire('process-abort'); resolve(); return; } abortProcessingRequestComplete = () => { resolve(); }; state.activeProcessor.abort(); }); // // logic to revert a processed file // const revert = (revertFileUpload, forceRevert) => new Promise((resolve, reject) => { // a completed upload will have a serverFileReference, a failed chunked upload where // getting a serverId succeeded but >=0 chunks have been uploaded will have transferId set const serverTransferId = state.serverFileReference !== null ? state.serverFileReference : state.transferId; // cannot revert without a server id for this process if (serverTransferId === null) { resolve(); return; } // revert the upload (fire and forget) revertFileUpload( serverTransferId, () => { // reset file server id and transfer id as now it's not available on the server state.serverFileReference = null; state.transferId = null; resolve(); }, error => { // don't set error state when reverting is optional, it will always resolve if (!forceRevert) { resolve(); return; } // oh no errors setStatus(ItemStatus.PROCESSING_REVERT_ERROR); fire('process-revert-error'); reject(error); } ); // fire event setStatus(ItemStatus.IDLE); fire('process-revert'); }); // exposed methods const setMetadata = (key, value, silent) => { const keys = key.split('.'); const root = keys[0]; const last = keys.pop(); let data = metadata; keys.forEach(key => (data = data[key])); // compare old value against new value, if they're the same, we're not updating if (JSON.stringify(data[last]) === JSON.stringify(value)) return; // update value data[last] = value; // fire update fire('metadata-update', { key: root, value: metadata[root], silent, }); }; const getMetadata = key => deepCloneObject(key ? metadata[key] : metadata); const api = { id: { get: () => id }, origin: { get: () => origin, set: value => (origin = value) }, serverId: { get: () => state.serverFileReference }, transferId: { get: () => state.transferId }, status: { get: () => state.status }, filename: { get: () => state.file.name }, filenameWithoutExtension: { get: () => getFilenameWithoutExtension(state.file.name) }, fileExtension: { get: getFileExtension }, fileType: { get: getFileType }, fileSize: { get: getFileSize }, file: { get: getFile }, relativePath: { get: () => state.file._relativePath }, source: { get: () => state.source }, getMetadata, setMetadata: (key, value, silent) => { if (isObject(key)) { const data = key; Object.keys(data).forEach(key => { setMetadata(key, data[key], value); }); return key; } setMetadata(key, value, silent); return value; }, extend: (name, handler) => (itemAPI[name] = handler), abortLoad, retryLoad, requestProcessing, abortProcessing, load, process, revert, ...on(), freeze: () => (state.frozen = true), release: () => (state.released = true), released: { get: () => state.released }, archive: () => (state.archived = true), archived: { get: () => state.archived }, // replace source and file object setFile: file => (state.file = file), }; // create it here instead of returning it instantly so we can extend it later const itemAPI = createObject(api); return itemAPI; }; ================================================ FILE: src/js/app/utils/createItemAPI.js ================================================ import { copyObjectPropertiesToObject } from '../../utils/copyObjectPropertiesToObject'; const PRIVATE = [ 'fire', 'process', 'revert', 'load', 'on', 'off', 'onOnce', 'retryLoad', 'extend', 'archive', 'archived', 'release', 'released', 'requestProcessing', 'freeze' ]; export const createItemAPI = item => { const api = {}; copyObjectPropertiesToObject(item, api, PRIVATE); return api; }; ================================================ FILE: src/js/app/utils/createOption.js ================================================ import { getValueByType } from './getValueByType'; export const createOption = (defaultValue, valueType) => { let currentValue = defaultValue; return { enumerable: true, get: () => currentValue, set: newValue => { currentValue = getValueByType(newValue, defaultValue, valueType); } }; }; ================================================ FILE: src/js/app/utils/createOptionAPI.js ================================================ import { fromCamels } from '../../utils/fromCamels'; import { forin } from '../../utils/forin'; export const createOptionAPI = (store, options) => { const obj = {}; forin(options, key => { obj[key] = { get: () => store.getState().options[key], set: value => { store.dispatch(`SET_${fromCamels(key, '_').toUpperCase()}`, { value }); } }; }); return obj; }; ================================================ FILE: src/js/app/utils/createOptionActions.js ================================================ import { fromCamels } from '../../utils/fromCamels'; import { forin } from '../../utils/forin'; export const createOptionActions = options => (dispatch, query, state) => { const obj = {}; forin(options, key => { const name = fromCamels(key, '_').toUpperCase(); obj[`SET_${name}`] = action => { try { state.options[key] = action.value; } catch (e) { // nope, failed } // we successfully set the value of this option dispatch(`DID_SET_${name}`, { value: state.options[key] }); }; }); return obj; }; ================================================ FILE: src/js/app/utils/createOptionQueries.js ================================================ import { fromCamels } from '../../utils/fromCamels'; import { forin } from '../../utils/forin'; export const createOptionQueries = options => state => { const obj = {}; forin(options, key => { obj[`GET_${fromCamels(key, '_').toUpperCase()}`] = action => state.options[key]; }); return obj; }; ================================================ FILE: src/js/app/utils/createOptions.js ================================================ import { createObject } from '../../utils/createObject'; import { createOption } from './createOption'; import { forin } from '../../utils/forin'; export const createOptions = options => { const obj = {}; forin(options, prop => { const optionDefinition = options[prop]; obj[prop] = createOption( optionDefinition[0], optionDefinition[1] ); }); return createObject(obj); }; ================================================ FILE: src/js/app/utils/createPaster.js ================================================ import { arrayRemove } from '../../utils/arrayRemove'; import { requestDataTransferItems } from './requestDataTransferItems'; let listening = false; const listeners = []; const handlePaste = e => { // if is pasting in input or textarea and the target is outside of a filepond scope, ignore const activeEl = document.activeElement; const isActiveElementEditable = activeEl && (/textarea|input/i.test(activeEl.nodeName) || activeEl.getAttribute('contenteditable') === 'true' || activeEl.getAttribute('contenteditable') === ''); if (isActiveElementEditable) { // test textarea or input is contained in filepond root let inScope = false; let element = activeEl; while (element !== document.body) { if (element.classList.contains('filepond--root')) { inScope = true; break; } element = element.parentNode; } if (!inScope) return; } requestDataTransferItems(e.clipboardData).then(files => { // no files received if (!files.length) { return; } // notify listeners of received files listeners.forEach(listener => listener(files)); }); }; const listen = cb => { // can't add twice if (listeners.includes(cb)) { return; } // add initial listener listeners.push(cb); // setup paste listener for entire page if (listening) { return; } listening = true; document.addEventListener('paste', handlePaste); }; const unlisten = listener => { arrayRemove(listeners, listeners.indexOf(listener)); // clean up if (listeners.length === 0) { document.removeEventListener('paste', handlePaste); listening = false; } }; export const createPaster = () => { const cb = files => { api.onload(files); }; const api = { destroy: () => { unlisten(cb); }, onload: () => {} }; listen(cb); return api; }; ================================================ FILE: src/js/app/utils/createPerceivedPerformanceUpdater.js ================================================ import { getRandomNumber } from '../../utils/getRandomNumber'; export const createPerceivedPerformanceUpdater = ( cb, duration = 1000, offset = 0, tickMin = 25, tickMax = 250 ) => { let timeout = null; const start = Date.now(); const tick = () => { let runtime = Date.now() - start; let delay = getRandomNumber(tickMin, tickMax); if (runtime + delay > duration) { delay = runtime + delay - duration; } let progress = runtime / duration; if (progress >= 1 || document.hidden) { cb(1); return; } cb(progress); timeout = setTimeout(tick, delay); }; if (duration > 0) tick(); return { clear: () => { clearTimeout(timeout); }, }; }; ================================================ FILE: src/js/app/utils/createProcessorFunction.js ================================================ import { isString } from '../../utils/isString'; import { createFileProcessorFunction } from './createFileProcessorFunction'; export const createProcessorFunction = (apiUrl = '', action, name, options) => { // custom handler (should also handle file, load, error, progress and abort) if (typeof action === 'function') return (...params) => action(name, ...params, options); // no action supplied if (!action || !isString(action.url)) return null; // internal handler return createFileProcessorFunction(apiUrl, action, name, options); }; ================================================ FILE: src/js/app/utils/createRevertFunction.js ================================================ import { sendRequest } from '../../utils/sendRequest'; import { createResponse } from '../../utils/createResponse'; import { createTimeoutResponse } from '../../utils/createDefaultResponse'; import { isString } from '../../utils/isString'; /* function signature: (uniqueFileId, load, error) => { } */ export const createRevertFunction = (apiUrl = '', action) => { // is custom implementation if (typeof action === 'function') { return action; } // no action supplied, return stub function, interface will work, but file won't be removed if (!action || !isString(action.url)) { return (uniqueFileId, load) => load(); } // set onload hanlder const onload = action.onload || (res => res); const onerror = action.onerror || (res => null); // internal implementation return (uniqueFileId, load, error) => { const request = sendRequest( uniqueFileId, apiUrl + action.url, action // contains method, headers and withCredentials properties ); request.onload = (xhr) => { load( createResponse( 'load', xhr.status, onload(xhr.response), xhr.getAllResponseHeaders() ) ) }; request.onerror = (xhr) => { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.ontimeout = createTimeoutResponse(error); return request; }; }; ================================================ FILE: src/js/app/utils/createServerAPI.js ================================================ import { isString } from '../../utils/isString'; import { toBoolean } from '../../utils/toBoolean'; import { forin } from '../../utils/forin'; const methods = { process: 'POST', patch: 'PATCH', revert: 'DELETE', fetch: 'GET', restore: 'GET', load: 'GET', }; export const createServerAPI = outline => { const api = {}; api.url = isString(outline) ? outline : outline.url || ''; api.timeout = outline.timeout ? parseInt(outline.timeout, 10) : 0; api.headers = outline.headers ? outline.headers : {}; forin(methods, key => { api[key] = createAction(key, outline[key], methods[key], api.timeout, api.headers); }); // remove process if no url or process on outline api.process = outline.process || isString(outline) || outline.url ? api.process : null; // special treatment for remove api.remove = outline.remove || null; // remove generic headers from api object delete api.headers; return api; }; const createAction = (name, outline, method, timeout, headers) => { // is explicitely set to null so disable if (outline === null) { return null; } // if is custom function, done! Dev handles everything. if (typeof outline === 'function') { return outline; } // build action object const action = { url: method === 'GET' || method === 'PATCH' ? `?${name}=` : '', method, headers, withCredentials: false, timeout, onload: null, ondata: null, onerror: null, }; // is a single url if (isString(outline)) { action.url = outline; return action; } // overwrite Object.assign(action, outline); // see if should reformat headers; if (isString(action.headers)) { const parts = action.headers.split(/:(.+)/); action.headers = { header: parts[0], value: parts[1], }; } // if is bool withCredentials action.withCredentials = toBoolean(action.withCredentials); return action; }; ================================================ FILE: src/js/app/utils/dnd.js ================================================ import { forin } from '../../utils/forin'; import { getRootNode } from '../../utils/getRootNode'; import { requestDataTransferItems } from './requestDataTransferItems'; const dragNDropObservers = []; const eventPosition = e => ({ pageLeft: e.pageX, pageTop: e.pageY, scopeLeft: e.offsetX || e.layerX, scopeTop: e.offsetY || e.layerY }); export const createDragNDropClient = ( element, scopeToObserve, filterElement ) => { const observer = getDragNDropObserver(scopeToObserve); const client = { element, filterElement, state: null, ondrop: () => {}, onenter: () => {}, ondrag: () => {}, onexit: () => {}, onload: () => {}, allowdrop: () => {} }; client.destroy = observer.addListener(client); return client; }; const getDragNDropObserver = element => { // see if already exists, if so, return const observer = dragNDropObservers.find(item => item.element === element); if (observer) { return observer; } // create new observer, does not yet exist for this element const newObserver = createDragNDropObserver(element); dragNDropObservers.push(newObserver); return newObserver; }; const createDragNDropObserver = element => { const clients = []; const routes = { dragenter, dragover, dragleave, drop }; const handlers = {}; forin(routes, (event, createHandler) => { handlers[event] = createHandler(element, clients); element.addEventListener(event, handlers[event], false); }); const observer = { element, addListener: client => { // add as client clients.push(client); // return removeListener function return () => { // remove client clients.splice(clients.indexOf(client), 1); // if no more clients, clean up observer if (clients.length === 0) { dragNDropObservers.splice( dragNDropObservers.indexOf(observer), 1 ); forin(routes, event => { element.removeEventListener( event, handlers[event], false ); }); } }; } }; return observer; }; const elementFromPoint = (root, point) => { if (!('elementFromPoint' in root)) { root = document; } return root.elementFromPoint(point.x, point.y); } const isEventTarget = (e, target) => { // get root const root = getRootNode(target); // get element at position // if root is not actual shadow DOM and does not have elementFromPoint method, use the one on document const elementAtPosition = elementFromPoint(root,{ x: e.pageX - window.pageXOffset, y: e.pageY - window.pageYOffset }); // test if target is the element or if one of its children is return elementAtPosition === target || target.contains(elementAtPosition); }; let initialTarget = null; const setDropEffect = (dataTransfer, effect) => { // is in try catch as IE11 will throw error if not try { dataTransfer.dropEffect = effect; } catch (e) {} }; const dragenter = (root, clients) => e => { e.preventDefault(); initialTarget = e.target; clients.forEach(client => { const { element, onenter } = client; if (isEventTarget(e, element)) { client.state = 'enter'; // fire enter event onenter(eventPosition(e)); } }); }; const dragover = (root, clients) => e => { e.preventDefault(); const dataTransfer = e.dataTransfer; requestDataTransferItems(dataTransfer).then(items => { let overDropTarget = false; clients.some(client => { const { filterElement, element, onenter, onexit, ondrag, allowdrop } = client; // by default we can drop setDropEffect(dataTransfer, 'copy'); // allow transfer of these items const allowsTransfer = allowdrop(items); // only used when can be dropped on page if (!allowsTransfer) { setDropEffect(dataTransfer, 'none'); return; } // targetting this client if (isEventTarget(e, element)) { overDropTarget = true; // had no previous state, means we are entering this client if (client.state === null) { client.state = 'enter'; onenter(eventPosition(e)); return; } // now over element (no matter if it allows the drop or not) client.state = 'over'; // needs to allow transfer if (filterElement && !allowsTransfer) { setDropEffect(dataTransfer, 'none'); return; } // dragging ondrag(eventPosition(e)); } else { // should be over an element to drop if (filterElement && !overDropTarget) { setDropEffect(dataTransfer, 'none'); } // might have just left this client? if (client.state) { client.state = null; onexit(eventPosition(e)); } } }); }); }; const drop = (root, clients) => e => { e.preventDefault(); const dataTransfer = e.dataTransfer; requestDataTransferItems(dataTransfer).then(items => { clients.forEach(client => { const { filterElement, element, ondrop, onexit, allowdrop } = client; client.state = null; // if we're filtering on element we need to be over the element to drop if (filterElement && !isEventTarget(e, element)) return; // no transfer for this client if (!allowdrop(items)) return onexit(eventPosition(e)); // we can drop these items on this client ondrop(eventPosition(e), items); }); }); }; const dragleave = (root, clients) => e => { if (initialTarget !== e.target) { return; } clients.forEach(client => { const { onexit } = client; client.state = null; onexit(eventPosition(e)); }); }; ================================================ FILE: src/js/app/utils/dropAreaDimensions.js ================================================ export const dropAreaDimensions = { height: 0, width: 0, get getHeight() { return this.height; }, set setHeight(val) { if (this.height === 0 || val === 0) this.height = val; }, get getWidth() { return this.width; }, set setWidth(val) { if (this.width === 0 || val === 0) this.width = val; }, setDimensions: function (height, width) { if (this.height === 0 || height === 0) this.height = height; if (this.width === 0 || width === 0) this.width = width; } }; ================================================ FILE: src/js/app/utils/dynamicLabel.js ================================================ import { isFunction } from '../../utils/isFunction'; export const dynamicLabel = (label) => (...params) => isFunction(label) ? label(...params) : label; ================================================ FILE: src/js/app/utils/fetchBlob.js ================================================ import { sendRequest } from '../../utils/sendRequest'; import { createResponse } from '../../utils/createResponse'; import { createTimeoutResponse } from '../../utils/createDefaultResponse'; import { getFileFromBlob } from '../../utils/getFileFromBlob'; import { getFileInfoFromHeaders } from '../../utils/getFileInfoFromHeaders'; import { getFilenameFromURL } from '../../utils/getFilenameFromURL'; export const fetchBlob = (url, load, error, progress, abort, headers) => { const request = sendRequest(null, url, { method: 'GET', responseType: 'blob' }); request.onload = (xhr) => { // get headers const headers = xhr.getAllResponseHeaders(); // get filename const filename = getFileInfoFromHeaders(headers).name || getFilenameFromURL(url); // create response load( createResponse( 'load', xhr.status, getFileFromBlob(xhr.response, filename), headers ) ) }; request.onerror = (xhr) => { error( createResponse( 'error', xhr.status, xhr.statusText, xhr.getAllResponseHeaders() ) ); }; request.onheaders = (xhr) => { headers( createResponse( 'headers', xhr.status, null, xhr.getAllResponseHeaders() ) ) }; request.ontimeout = createTimeoutResponse(error); request.onprogress = progress; request.onabort = abort; // should return request return request; }; ================================================ FILE: src/js/app/utils/getActiveItems.js ================================================ export const getActiveItems = (items) => items.filter(item => !item.archived); ================================================ FILE: src/js/app/utils/getItemById.js ================================================ import { getItemIndexByQuery } from './getItemIndexByQuery'; export const getItemById = (items, itemId) => { const index = getItemIndexByQuery(items, itemId); if (index < 0) { return; } return items[index] || null; }; ================================================ FILE: src/js/app/utils/getItemByQuery.js ================================================ import { isEmpty } from '../../utils/isEmpty'; import { isInt } from '../../utils/isInt'; export const getItemByQuery = (items, query) => { // just return first index if (isEmpty(query)) { return items[0] || null; } // query is index if (isInt(query)) { return items[query] || null; } // if query is item, get the id if (typeof query === 'object') { query = query.id; } // assume query is a string and return item by id return items.find(item => item.id === query) || null; }; ================================================ FILE: src/js/app/utils/getItemIndexByPosition.js ================================================ import getItemsPerRow from './getItemsPerRow'; export const getItemIndexByPosition = (view, children, positionInView) => { if (!positionInView) return; const horizontalSpace = view.rect.element.width; // const children = view.childViews; const l = children.length; let last = null; // -1, don't move items to accomodate (either add to top or bottom) if (l === 0 || positionInView.top < children[0].rect.element.top) return -1; // let's get the item width const item = children[0]; const itemRect = item.rect.element; const itemHorizontalMargin = itemRect.marginLeft + itemRect.marginRight; const itemWidth = itemRect.width + itemHorizontalMargin; const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth); // stack if (itemsPerRow === 1) { for (let index = 0; index < l; index++) { const child = children[index]; const childMid = child.rect.outer.top + child.rect.element.height * 0.5; if (positionInView.top < childMid) { return index; } } return l; } // grid const itemVerticalMargin = itemRect.marginTop + itemRect.marginBottom; const itemHeight = itemRect.height + itemVerticalMargin; for (let index = 0; index < l; index++) { const indexX = index % itemsPerRow; const indexY = Math.floor(index / itemsPerRow); const offsetX = indexX * itemWidth; const offsetY = indexY * itemHeight; const itemTop = offsetY - itemRect.marginTop; const itemRight = offsetX + itemWidth; const itemBottom = offsetY + itemHeight + itemRect.marginBottom; if (positionInView.top < itemBottom && positionInView.top > itemTop) { if (positionInView.left < itemRight) { return index; } else if (index !== l - 1) { last = index; } else { last = null; } } } if (last !== null) { return last; } return l; }; ================================================ FILE: src/js/app/utils/getItemIndexByQuery.js ================================================ import { isEmpty } from '../../utils/isEmpty'; import { isString } from '../../utils/isString'; export const getItemIndexByQuery = (items, query) => { // just return first index if (isEmpty(query)) { return 0; } // invalid queries if (!isString(query)) { return -1; } // return item by id (or -1 if not found) return items.findIndex(item => item.id === query); }; ================================================ FILE: src/js/app/utils/getItemsPerRow.js ================================================ export default (horizontalSpace, itemWidth) => { // add one pixel leeway, when using percentages for item width total items can be 1.99 per row return Math.max(1, Math.floor((horizontalSpace + 1) / itemWidth)); }; ================================================ FILE: src/js/app/utils/getType.js ================================================ import { isArray } from '../../utils/isArray'; import { isInt } from '../../utils/isInt'; import { isNull } from '../../utils/isNull'; import { isAPI } from './isAPI'; export const getType = value => { if (isArray(value)) { return 'array'; } if (isNull(value)) { return 'null'; } if (isInt(value)) { return 'int'; } if (/^[0-9]+ ?(?:GB|MB|KB)$/gi.test(value)) { return 'bytes'; } if (isAPI(value)) { return 'api'; } return typeof value; }; ================================================ FILE: src/js/app/utils/getValueByType.js ================================================ import { convertTo } from './convertTo'; import { getType } from './getType'; export const getValueByType = (newValue, defaultValue, valueType) => { // can always assign default value if (newValue === defaultValue) { return newValue; } // get the type of the new value let newValueType = getType(newValue); // is valid type? if (newValueType !== valueType) { // is string input, let's attempt to convert const convertedValue = convertTo(newValue, valueType); // what is the type now newValueType = getType(convertedValue); // no valid conversions found if (convertedValue === null) { throw `Trying to assign value with incorrect type to "${option}", allowed type: "${valueType}"`; } else { newValue = convertedValue; } } // assign new value return newValue; } ================================================ FILE: src/js/app/utils/hasRoomForItem.js ================================================ import { getActiveItems } from './getActiveItems'; export const hasRoomForItem = state => { const count = getActiveItems(state.items).length; // if cannot have multiple items, to add one item it should currently not contain items if (!state.options.allowMultiple) { return count === 0; } // if allows multiple items, we check if a max item count has been set, if not, there's no limit const maxFileCount = state.options.maxFiles; if (maxFileCount === null) { return true; } // we check if the current count is smaller than the max count, if so, another file can still be added if (count < maxFileCount) { return true; } // no more room for another file return false; }; ================================================ FILE: src/js/app/utils/insertItem.js ================================================ import { isEmpty } from '../../utils/isEmpty'; import { limit } from '../../utils/limit'; import { arrayInsert } from '../../utils/arrayInsert'; export const insertItem = (items, item, index) => { if (isEmpty(item)) { return null; } // if index is undefined, append if (typeof index === 'undefined') { items.push(item); return item; } // limit the index to the size of the items array index = limit(index, 0, items.length); // add item to array arrayInsert(items, index, item); // expose return item; }; ================================================ FILE: src/js/app/utils/isAPI.js ================================================ import { isObject } from '../../utils/isObject'; import { isString } from '../../utils/isString'; export const isAPI = value => { return ( isObject(value) && isString(value.url) && isObject(value.process) && isObject(value.revert) && isObject(value.restore) && isObject(value.fetch) ); }; ================================================ FILE: src/js/app/utils/mergeOptionObject.js ================================================ import { forin } from '../../utils/forin'; export const mergeOptionObject = (originalObject, newObject) => { const obj = {}; forin(originalObject, key => { obj[key] = newObject[key] || originalObject[key]; }); return obj; }; ================================================ FILE: src/js/app/utils/on.js ================================================ import { arrayRemove } from '../../utils/arrayRemove'; const run = (cb, sync) => { if (sync) { cb(); } else if (document.hidden) { Promise.resolve(1).then(cb); } else { setTimeout(cb, 0); } } export const on = () => { const listeners = []; const off = (event, cb) => { arrayRemove( listeners, listeners.findIndex( listener => listener.event === event && (listener.cb === cb || !cb) ) ); }; const fire = (event, args, sync) => { listeners .filter(listener => listener.event === event) .map(listener => listener.cb) .forEach(cb => run(() => cb(...args), sync)); } return { fireSync: (event, ...args) => { fire(event, args, true); }, fire: (event, ...args) => { fire(event, args, false); }, on: (event, cb) => { listeners.push({ event, cb }); }, onOnce: (event, cb) => { listeners.push({ event, cb: (...args) => { off(event, cb); cb(...args); } }); }, off }; }; ================================================ FILE: src/js/app/utils/processFileChunked.js ================================================ import { sendRequest } from '../../utils/sendRequest'; import { createResponse } from '../../utils/createResponse'; import { createTimeoutResponse } from '../../utils/createDefaultResponse'; import { isObject } from '../../utils/isObject'; import { buildURL } from './buildURL'; import { ChunkStatus } from '../enum/ChunkStatus'; /* function signature: (file, metadata, load, error, progress, abort, transfer, options) => { return { abort:() => {} } } */ // apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options export const processFileChunked = ( apiUrl, action, name, file, metadata, load, error, progress, abort, transfer, options ) => { // all chunks const chunks = []; const { chunkTransferId, chunkServer, chunkSize, chunkRetryDelays } = options; // default state const state = { serverId: chunkTransferId, aborted: false, }; // set onload handlers const ondata = action.ondata || (fd => fd); const onload = action.onload || ((xhr, method) => method === 'HEAD' ? xhr.getResponseHeader('Upload-Offset') : xhr.response); const onerror = action.onerror || (res => null); // create server hook const requestTransferId = cb => { const formData = new FormData(); // add metadata under same name if (isObject(metadata)) formData.append(name, JSON.stringify(metadata)); const headers = typeof action.headers === 'function' ? action.headers(file, metadata) : { ...action.headers, 'Upload-Length': file.size, }; const requestParams = { ...action, headers, }; // send request object const request = sendRequest(ondata(formData), buildURL(apiUrl, action.url), requestParams); request.onload = xhr => cb(onload(xhr, requestParams.method)); request.onerror = xhr => error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); request.ontimeout = createTimeoutResponse(error); }; const requestTransferOffset = cb => { const requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId); const headers = typeof action.headers === 'function' ? action.headers(state.serverId) : { ...action.headers, }; const requestParams = { headers, method: 'HEAD', }; const request = sendRequest(null, requestUrl, requestParams); request.onload = xhr => cb(onload(xhr, requestParams.method)); request.onerror = xhr => error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); request.ontimeout = createTimeoutResponse(error); }; // create chunks const lastChunkIndex = Math.floor(file.size / chunkSize); for (let i = 0; i <= lastChunkIndex; i++) { const offset = i * chunkSize; const data = file.slice(offset, offset + chunkSize, 'application/offset+octet-stream'); chunks[i] = { index: i, size: data.size, offset, data, file, progress: 0, retries: [...chunkRetryDelays], status: ChunkStatus.QUEUED, error: null, request: null, timeout: null, }; } const completeProcessingChunks = () => load(state.serverId); const canProcessChunk = chunk => chunk.status === ChunkStatus.QUEUED || chunk.status === ChunkStatus.ERROR; const processChunk = chunk => { // processing is paused, wait here if (state.aborted) return; // get next chunk to process chunk = chunk || chunks.find(canProcessChunk); // no more chunks to process if (!chunk) { // all done? if (chunks.every(chunk => chunk.status === ChunkStatus.COMPLETE)) { completeProcessingChunks(); } // no chunk to handle return; } // now processing this chunk chunk.status = ChunkStatus.PROCESSING; chunk.progress = null; // allow parsing of formdata const ondata = chunkServer.ondata || (fd => fd); const onerror = chunkServer.onerror || (res => null); const onload = chunkServer.onload || (() => {}); // send request object const requestUrl = buildURL(apiUrl, chunkServer.url, state.serverId); const headers = typeof chunkServer.headers === 'function' ? chunkServer.headers(chunk) : { ...chunkServer.headers, 'Content-Type': 'application/offset+octet-stream', 'Upload-Offset': chunk.offset, 'Upload-Length': file.size, 'Upload-Name': file.name, }; const request = (chunk.request = sendRequest(ondata(chunk.data), requestUrl, { ...chunkServer, headers, })); request.onload = xhr => { // allow hooking into request result onload(xhr, chunk.index, chunks.length); // done! chunk.status = ChunkStatus.COMPLETE; // remove request reference chunk.request = null; // start processing more chunks processChunks(); }; request.onprogress = (lengthComputable, loaded, total) => { chunk.progress = lengthComputable ? loaded : null; updateTotalProgress(); }; request.onerror = xhr => { chunk.status = ChunkStatus.ERROR; chunk.request = null; chunk.error = onerror(xhr.response) || xhr.statusText; if (!retryProcessChunk(chunk)) { error( createResponse( 'error', xhr.status, onerror(xhr.response) || xhr.statusText, xhr.getAllResponseHeaders() ) ); } }; request.ontimeout = xhr => { chunk.status = ChunkStatus.ERROR; chunk.request = null; if (!retryProcessChunk(chunk)) { createTimeoutResponse(error)(xhr); } }; request.onabort = () => { chunk.status = ChunkStatus.QUEUED; chunk.request = null; abort(); }; }; const retryProcessChunk = chunk => { // no more retries left if (chunk.retries.length === 0) return false; // new retry chunk.status = ChunkStatus.WAITING; clearTimeout(chunk.timeout); chunk.timeout = setTimeout(() => { processChunk(chunk); }, chunk.retries.shift()); // we're going to retry return true; }; const updateTotalProgress = () => { // calculate total progress fraction const totalBytesTransfered = chunks.reduce((p, chunk) => { if (p === null || chunk.progress === null) return null; return p + chunk.progress; }, 0); // can't compute progress if (totalBytesTransfered === null) return progress(false, 0, 0); // calculate progress values const totalSize = chunks.reduce((total, chunk) => total + chunk.size, 0); // can update progress indicator progress(true, totalBytesTransfered, totalSize); }; // process new chunks const processChunks = () => { const totalProcessing = chunks.filter(chunk => chunk.status === ChunkStatus.PROCESSING) .length; if (totalProcessing >= 1) return; processChunk(); }; const abortChunks = () => { chunks.forEach(chunk => { clearTimeout(chunk.timeout); if (chunk.request) { chunk.request.abort(); } }); }; // let's go! if (!state.serverId) { requestTransferId(serverId => { // stop here if aborted, might have happened in between request and callback if (state.aborted) return; // pass back to item so we can use it if something goes wrong transfer(serverId); // store internally state.serverId = serverId; processChunks(); }); } else { requestTransferOffset(offset => { // stop here if aborted, might have happened in between request and callback if (state.aborted) return; // mark chunks with lower offset as complete chunks .filter(chunk => chunk.offset < offset) .forEach(chunk => { chunk.status = ChunkStatus.COMPLETE; chunk.progress = chunk.size; }); // continue processing processChunks(); }); } return { abort: () => { state.aborted = true; abortChunks(); }, }; }; ================================================ FILE: src/js/app/utils/removeReleasedItems.js ================================================ import { arrayRemove } from '../../utils/arrayRemove'; export const removeReleasedItems = (items) => { items.forEach((item, index) => { if (item.released) { arrayRemove(items, index); } }); }; ================================================ FILE: src/js/app/utils/requestDataTransferItems.js ================================================ import { guesstimateMimeType } from '../../utils/guesstimateMimeType'; import { getExtensionFromFilename } from '../../utils/getExtensionFromFilename'; export const requestDataTransferItems = dataTransfer => new Promise((resolve, reject) => { // try to get links from transfer, if found we'll exit immediately (unless a file is in the dataTransfer as well, this is because Firefox could represent the file as a URL and a file object at the same time) const links = getLinks(dataTransfer); if (links.length && !hasFiles(dataTransfer)) { return resolve(links); } // try to get files from the transfer getFiles(dataTransfer).then(resolve); }); /** * Test if datatransfer has files */ const hasFiles = (dataTransfer) => { if (dataTransfer.files) return dataTransfer.files.length > 0; return false; } /** * Extracts files from a DataTransfer object */ const getFiles = dataTransfer => new Promise((resolve, reject) => { // get the transfer items as promises const promisedFiles = (dataTransfer.items ? Array.from(dataTransfer.items) : [] ) // only keep file system items (files and directories) .filter(item => isFileSystemItem(item)) // map each item to promise .map(item => getFilesFromItem(item)); // if is empty, see if we can extract some info from the files property as a fallback if (!promisedFiles.length) { // TODO: test for directories (should not be allowed) // Use FileReader, problem is that the files property gets lost in the process resolve(dataTransfer.files ? Array.from(dataTransfer.files) : []); return; } // done! Promise.all(promisedFiles) .then(returnedFileGroups => { // flatten groups const files = []; returnedFileGroups.forEach(group => { files.push.apply(files, group); }); // done (filter out empty files)! resolve(files.filter(file => file).map(file => { if (!file._relativePath) file._relativePath = file.webkitRelativePath return file; })); }) .catch(console.error); }); const isFileSystemItem = item => { if (isEntry(item)) { const entry = getAsEntry(item); if (entry) { return entry.isFile || entry.isDirectory; } } return item.kind === 'file'; }; const getFilesFromItem = item => new Promise((resolve, reject) => { if (isDirectoryEntry(item)) { getFilesInDirectory(getAsEntry(item)) .then(resolve) .catch(reject) return; } resolve([item.getAsFile()]); }); const getFilesInDirectory = entry => new Promise((resolve, reject) => { const files = []; // the total entries to read let dirCounter = 0; let fileCounter = 0; const resolveIfDone = () => { if (fileCounter === 0 && dirCounter === 0) { resolve(files); } } // the recursive function const readEntries = dirEntry => { dirCounter++; const directoryReader = dirEntry.createReader(); // directories are returned in batches, we need to process all batches before we're done const readBatch = () => { directoryReader.readEntries(entries => { if (entries.length === 0) { dirCounter--; resolveIfDone(); return; } entries.forEach(entry => { // recursively read more directories if (entry.isDirectory) { readEntries(entry); } else { // read as file fileCounter++; entry.file(file => { const correctedFile = correctMissingFileType(file); if (entry.fullPath) correctedFile._relativePath = entry.fullPath; files.push(correctedFile); fileCounter--; resolveIfDone(); }); } }); // try to get next batch of files readBatch(); }, reject); } // read first batch of files readBatch(); }; // go! readEntries(entry); }); const correctMissingFileType = (file) => { if (file.type.length) return file; const date = file.lastModifiedDate; const name = file.name; const type = guesstimateMimeType(getExtensionFromFilename(file.name)); if (!type.length) return file; file = file.slice(0, file.size, type); file.name = name; file.lastModifiedDate = date; return file; } const isDirectoryEntry = item => isEntry(item) && (getAsEntry(item) || {}).isDirectory; const isEntry = item => 'webkitGetAsEntry' in item; const getAsEntry = item => item.webkitGetAsEntry(); /** * Extracts links from a DataTransfer object */ const getLinks = dataTransfer => { let links = []; try { // look in meta data property links = getLinksFromTransferMetaData(dataTransfer); if (links.length) { return links; } links = getLinksFromTransferURLData(dataTransfer); } catch (e) { // nope nope nope (probably IE trouble) } return links; }; const getLinksFromTransferURLData = dataTransfer => { let data = dataTransfer.getData('url'); if (typeof data === 'string' && data.length) { return [data]; } return []; }; const getLinksFromTransferMetaData = dataTransfer => { let data = dataTransfer.getData('text/html'); if (typeof data === 'string' && data.length) { const matches = data.match(/src\s*=\s*"(.+?)"/); if (matches) { return [matches[1]]; } } return []; }; ================================================ FILE: src/js/app/utils/toServerAPI.js ================================================ import { createServerAPI } from './createServerAPI'; export const toServerAPI = value => createServerAPI(value); ================================================ FILE: src/js/app/view/assistant.js ================================================ import { createView, createRoute } from '../frame/index'; import { attr } from '../../utils/attr'; /** * Creates the file view */ const create = ({ root, props }) => { root.element.id = `filepond--assistant-${props.id}`; attr(root.element, 'role', 'alert'); attr(root.element, 'aria-live', 'polite'); attr(root.element, 'aria-relevant', 'additions'); }; let addFilesNotificationTimeout = null; let notificationClearTimeout = null; const filenames = []; const assist = (root, message) => { root.element.textContent = message; }; const clear = root => { root.element.textContent = ''; }; const listModified = (root, filename, label) => { const total = root.query('GET_TOTAL_ITEMS'); assist( root, `${label} ${filename}, ${total} ${ total === 1 ? root.query('GET_LABEL_FILE_COUNT_SINGULAR') : root.query('GET_LABEL_FILE_COUNT_PLURAL') }` ); // clear group after set amount of time so the status is not read twice clearTimeout(notificationClearTimeout); notificationClearTimeout = setTimeout(() => { clear(root); }, 1500); }; const isUsingFilePond = root => root.element.parentNode.contains(document.activeElement); const itemAdded = ({ root, action }) => { if (!isUsingFilePond(root)) { return; } root.element.textContent = ''; const item = root.query('GET_ITEM', action.id); filenames.push(item.filename); clearTimeout(addFilesNotificationTimeout); addFilesNotificationTimeout = setTimeout(() => { listModified(root, filenames.join(', '), root.query('GET_LABEL_FILE_ADDED')); filenames.length = 0; }, 750); }; const itemRemoved = ({ root, action }) => { if (!isUsingFilePond(root)) { return; } const item = action.item; listModified(root, item.filename, root.query('GET_LABEL_FILE_REMOVED')); }; const itemProcessed = ({ root, action }) => { // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file const item = root.query('GET_ITEM', action.id); const filename = item.filename; const label = root.query('GET_LABEL_FILE_PROCESSING_COMPLETE'); assist(root, `${filename} ${label}`); }; const itemProcessedUndo = ({ root, action }) => { const item = root.query('GET_ITEM', action.id); const filename = item.filename; const label = root.query('GET_LABEL_FILE_PROCESSING_ABORTED'); assist(root, `${filename} ${label}`); }; const itemError = ({ root, action }) => { const item = root.query('GET_ITEM', action.id); const filename = item.filename; // will also notify the user when FilePond is not being used, as the user might be occupied with other activities while uploading a file assist(root, `${action.status.main} ${filename} ${action.status.sub}`); }; export const assistant = createView({ create, ignoreRect: true, ignoreRectUpdate: true, write: createRoute({ DID_LOAD_ITEM: itemAdded, DID_REMOVE_ITEM: itemRemoved, DID_COMPLETE_ITEM_PROCESSING: itemProcessed, DID_ABORT_ITEM_PROCESSING: itemProcessedUndo, DID_REVERT_ITEM_PROCESSING: itemProcessedUndo, DID_THROW_ITEM_REMOVE_ERROR: itemError, DID_THROW_ITEM_LOAD_ERROR: itemError, DID_THROW_ITEM_INVALID: itemError, DID_THROW_ITEM_PROCESSING_ERROR: itemError, }), tag: 'span', name: 'assistant', }); ================================================ FILE: src/js/app/view/blob.js ================================================ import { createView } from '../frame/index'; export const blob = createView({ name: 'drip-blob', ignoreRect: true, mixins: { styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'], animations: { scaleX: 'spring', scaleY: 'spring', translateX: 'spring', translateY: 'spring', opacity: { type: 'tween', duration: 250 } } } }); ================================================ FILE: src/js/app/view/browser.js ================================================ import { createView, createRoute } from '../frame/index'; import { attrToggle } from '../../utils/attrToggle'; import { resetFileInput } from '../../utils/resetFileInput'; import { attr } from '../../utils/attr'; import { ItemStatus } from '../enum/ItemStatus'; const create = ({ root, props }) => { // set id so can be referenced from outside labels root.element.id = `filepond--browser-${props.id}`; // set name of element (is removed when a value is set) attr(root.element, 'name', root.query('GET_NAME')); // we have to link this element to the status element attr(root.element, 'aria-controls', `filepond--assistant-${props.id}`); // set label, we use labelled by as otherwise the screenreader does not read the "browse" text in the label (as it has tabindex: 0) attr(root.element, 'aria-labelledby', `filepond--drop-label-${props.id}`); // set configurable props setAcceptedFileTypes({ root, action: { value: root.query('GET_ACCEPTED_FILE_TYPES') } }); toggleAllowMultiple({ root, action: { value: root.query('GET_ALLOW_MULTIPLE') } }); toggleDirectoryFilter({ root, action: { value: root.query('GET_ALLOW_DIRECTORIES_ONLY') } }); toggleDisabled({ root }); toggleRequired({ root, action: { value: root.query('GET_REQUIRED') } }); setCaptureMethod({ root, action: { value: root.query('GET_CAPTURE_METHOD') } }); // handle changes to the input field root.ref.handleChange = e => { if (!root.element.value) { return; } // extract files and move value of webkitRelativePath path to _relativePath const files = Array.from(root.element.files).map(file => { file._relativePath = file.webkitRelativePath; return file; }); // we add a little delay so the OS file select window can move out of the way before we add our file setTimeout(() => { // load files props.onload(files); // reset input, it's just for exposing a method to drop files, should not retain any state resetFileInput(root.element); }, 250); }; root.element.addEventListener('change', root.ref.handleChange); }; const setAcceptedFileTypes = ({ root, action }) => { if (!root.query('GET_ALLOW_SYNC_ACCEPT_ATTRIBUTE')) return; attrToggle(root.element, 'accept', !!action.value, action.value ? action.value.join(',') : ''); }; const toggleAllowMultiple = ({ root, action }) => { attrToggle(root.element, 'multiple', action.value); }; const toggleDirectoryFilter = ({ root, action }) => { attrToggle(root.element, 'webkitdirectory', action.value); }; const toggleDisabled = ({ root }) => { const isDisabled = root.query('GET_DISABLED'); const doesAllowBrowse = root.query('GET_ALLOW_BROWSE'); const disableField = isDisabled || !doesAllowBrowse; attrToggle(root.element, 'disabled', disableField); }; const toggleRequired = ({ root, action }) => { // want to remove required, always possible if (!action.value) { attrToggle(root.element, 'required', false); } // if want to make required, only possible when zero items else if (root.query('GET_TOTAL_ITEMS') === 0) { attrToggle(root.element, 'required', true); } }; const setCaptureMethod = ({ root, action }) => { attrToggle(root.element, 'capture', !!action.value, action.value === true ? '' : action.value); }; const updateRequiredStatus = ({ root }) => { const { element } = root; // always remove the required attribute when more than zero items if (root.query('GET_TOTAL_ITEMS') > 0) { attrToggle(element, 'required', false); attrToggle(element, 'name', false); // still has items const activeItems = root.query('GET_ACTIVE_ITEMS'); let hasInvalidField = false; for (let i = 0; i < activeItems.length; i++) { if (activeItems[i].status === ItemStatus.LOAD_ERROR) { hasInvalidField = true; } } // set validity status root.element.setCustomValidity( hasInvalidField ? root.query('GET_LABEL_INVALID_FIELD') : '' ); } else { // add name attribute attrToggle(element, 'name', true, root.query('GET_NAME')); // remove any validation messages const shouldCheckValidity = root.query('GET_CHECK_VALIDITY'); if (shouldCheckValidity) { element.setCustomValidity(''); } // we only add required if the field has been deemed required if (root.query('GET_REQUIRED')) { attrToggle(element, 'required', true); } } }; const updateFieldValidityStatus = ({ root }) => { const shouldCheckValidity = root.query('GET_CHECK_VALIDITY'); if (!shouldCheckValidity) return; root.element.setCustomValidity(root.query('GET_LABEL_INVALID_FIELD')); }; export const browser = createView({ tag: 'input', name: 'browser', ignoreRect: true, ignoreRectUpdate: true, attributes: { type: 'file', }, create, destroy: ({ root }) => { root.element.removeEventListener('change', root.ref.handleChange); }, write: createRoute({ DID_LOAD_ITEM: updateRequiredStatus, DID_REMOVE_ITEM: updateRequiredStatus, DID_THROW_ITEM_INVALID: updateFieldValidityStatus, DID_SET_DISABLED: toggleDisabled, DID_SET_ALLOW_BROWSE: toggleDisabled, DID_SET_ALLOW_DIRECTORIES_ONLY: toggleDirectoryFilter, DID_SET_ALLOW_MULTIPLE: toggleAllowMultiple, DID_SET_ACCEPTED_FILE_TYPES: setAcceptedFileTypes, DID_SET_CAPTURE_METHOD: setCaptureMethod, DID_SET_REQUIRED: toggleRequired, }), }); ================================================ FILE: src/js/app/view/data.js ================================================ import { createView, createRoute } from '../frame/index'; import { createElement } from '../../utils/createElement'; import { setInputFiles } from '../../utils/setInputFiles'; import { FileOrigin } from '../enum/FileOrigin'; const create = ({ root }) => { root.ref.fields = {}; const legend = document.createElement('legend'); legend.textContent = 'Files'; root.element.appendChild(legend); }; const getField = (root, id) => root.ref.fields[id]; const syncFieldPositionsWithItems = root => { root.query('GET_ACTIVE_ITEMS').forEach(item => { if (!root.ref.fields[item.id]) return; root.element.appendChild(root.ref.fields[item.id]); }); }; const didReorderItems = ({ root }) => syncFieldPositionsWithItems(root); const didAddItem = ({ root, action }) => { const fileItem = root.query('GET_ITEM', action.id); const isLocalFile = fileItem.origin === FileOrigin.LOCAL; const shouldUseFileInput = !isLocalFile && root.query('SHOULD_UPDATE_FILE_INPUT'); const dataContainer = createElement('input'); dataContainer.type = shouldUseFileInput ? 'file' : 'hidden'; dataContainer.name = root.query('GET_NAME'); root.ref.fields[action.id] = dataContainer; syncFieldPositionsWithItems(root); }; const didLoadItem = ({ root, action }) => { const field = getField(root, action.id); if (!field) return; // store server ref in hidden input if (action.serverFileReference !== null) field.value = action.serverFileReference; // store file item in file input if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return; const fileItem = root.query('GET_ITEM', action.id); setInputFiles(field, [fileItem.file]); }; const didPrepareOutput = ({ root, action }) => { // this timeout pushes the handler after 'load' if (!root.query('SHOULD_UPDATE_FILE_INPUT')) return; setTimeout(() => { const field = getField(root, action.id); if (!field) return; setInputFiles(field, [action.file]); }, 0); }; const didSetDisabled = ({ root }) => { root.element.disabled = root.query('GET_DISABLED'); }; const didRemoveItem = ({ root, action }) => { const field = getField(root, action.id); if (!field) return; if (field.parentNode) field.parentNode.removeChild(field); delete root.ref.fields[action.id]; }; // only runs for server files. will refuse to update the value if the field // is a file field const didDefineValue = ({ root, action }) => { const field = getField(root, action.id); if (!field) return; if (action.value === null) { // clear field value field.removeAttribute('value'); } else { // set field value if (field.type != 'file') { field.value = action.value; } } syncFieldPositionsWithItems(root); }; const write = createRoute({ DID_SET_DISABLED: didSetDisabled, DID_ADD_ITEM: didAddItem, DID_LOAD_ITEM: didLoadItem, DID_REMOVE_ITEM: didRemoveItem, DID_DEFINE_VALUE: didDefineValue, DID_PREPARE_OUTPUT: didPrepareOutput, DID_REORDER_ITEMS: didReorderItems, DID_SORT_ITEMS: didReorderItems, }); export const data = createView({ tag: 'fieldset', name: 'data', create, write, ignoreRect: true }); ================================================ FILE: src/js/app/view/drip.js ================================================ import { createView, createRoute } from '../frame/index'; import { blob } from './blob'; const addBlob = ({ root }) => { const centerX = root.rect.element.width * 0.5; const centerY = root.rect.element.height * 0.5; root.ref.blob = root.appendChildView( root.createChildView(blob, { opacity: 0, scaleX: 2.5, scaleY: 2.5, translateX: centerX, translateY: centerY }) ); }; const moveBlob = ({ root, action }) => { if (!root.ref.blob) { addBlob({ root }); return; } root.ref.blob.translateX = action.position.scopeLeft; root.ref.blob.translateY = action.position.scopeTop; root.ref.blob.scaleX = 1; root.ref.blob.scaleY = 1; root.ref.blob.opacity = 1; }; const hideBlob = ({ root }) => { if (!root.ref.blob) { return; } root.ref.blob.opacity = 0; }; const explodeBlob = ({ root }) => { if (!root.ref.blob) { return; } root.ref.blob.scaleX = 2.5; root.ref.blob.scaleY = 2.5; root.ref.blob.opacity = 0; }; const write = ({ root, props, actions }) => { route({ root, props, actions }); const { blob } = root.ref; if (actions.length === 0 && blob && blob.opacity === 0) { root.removeChildView(blob); root.ref.blob = null; } }; const route = createRoute({ DID_DRAG: moveBlob, DID_DROP: explodeBlob, DID_END_DRAG: hideBlob }); export const drip = createView({ ignoreRect: true, ignoreRectUpdate: true, name: 'drip', write }); ================================================ FILE: src/js/app/view/dropLabel.js ================================================ import { createView, createRoute } from '../frame/index'; import { attr } from '../../utils/attr'; import { createElement } from '../../utils/createElement'; import { Key } from '../enum/Key'; const create = ({ root, props }) => { // create the label and link it to the file browser const label = createElement('label'); attr(label, 'for', `filepond--browser-${props.id}`); // use for labeling file input (aria-labelledby on file input) attr(label, 'id', `filepond--drop-label-${props.id}`); // handle keys root.ref.handleKeyDown = e => { const isActivationKey = e.keyCode === Key.ENTER || e.keyCode === Key.SPACE; if (!isActivationKey) return; // stops from triggering the element a second time e.preventDefault(); // click link (will then in turn activate file input) root.ref.label.click(); }; root.ref.handleClick = e => { const isLabelClick = e.target === label || label.contains(e.target); // don't want to click twice if (isLabelClick) return; // click link (will then in turn activate file input) root.ref.label.click(); }; // attach events label.addEventListener('keydown', root.ref.handleKeyDown); root.element.addEventListener('click', root.ref.handleClick); // update updateLabelValue(label, props.caption); // add! root.appendChild(label); root.ref.label = label; }; const updateLabelValue = (label, value) => { label.innerHTML = value; const clickable = label.querySelector('.filepond--label-action'); if (clickable) { attr(clickable, 'tabindex', '0'); } return value; }; export const dropLabel = createView({ name: 'drop-label', ignoreRect: true, create, destroy: ({ root }) => { root.ref.label.addEventListener('keydown', root.ref.handleKeyDown); root.element.removeEventListener('click', root.ref.handleClick); }, write: createRoute({ DID_SET_LABEL_IDLE: ({ root, action }) => { updateLabelValue(root.ref.label, action.value); }, }), mixins: { styles: ['opacity', 'translateX', 'translateY'], animations: { opacity: { type: 'tween', duration: 150 }, translateX: 'spring', translateY: 'spring', }, }, }); ================================================ FILE: src/js/app/view/file.js ================================================ import { createView, createRoute } from '../frame/index'; import { progressIndicator } from './progressIndicator'; import { fileActionButton } from './fileActionButton'; import { fileInfo } from './fileInfo'; import { fileStatus } from './fileStatus'; import { forin } from '../../utils/forin'; import { applyFilters } from '../../filter'; /** * Button definitions for the file view */ const Buttons = { AbortItemLoad: { label: 'GET_LABEL_BUTTON_ABORT_ITEM_LOAD', action: 'ABORT_ITEM_LOAD', className: 'filepond--action-abort-item-load', align: 'LOAD_INDICATOR_POSITION', // right }, RetryItemLoad: { label: 'GET_LABEL_BUTTON_RETRY_ITEM_LOAD', action: 'RETRY_ITEM_LOAD', icon: 'GET_ICON_RETRY', className: 'filepond--action-retry-item-load', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, RemoveItem: { label: 'GET_LABEL_BUTTON_REMOVE_ITEM', action: 'REQUEST_REMOVE_ITEM', icon: 'GET_ICON_REMOVE', className: 'filepond--action-remove-item', align: 'BUTTON_REMOVE_ITEM_POSITION', // left }, ProcessItem: { label: 'GET_LABEL_BUTTON_PROCESS_ITEM', action: 'REQUEST_ITEM_PROCESSING', icon: 'GET_ICON_PROCESS', className: 'filepond--action-process-item', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, AbortItemProcessing: { label: 'GET_LABEL_BUTTON_ABORT_ITEM_PROCESSING', action: 'ABORT_ITEM_PROCESSING', className: 'filepond--action-abort-item-processing', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, RetryItemProcessing: { label: 'GET_LABEL_BUTTON_RETRY_ITEM_PROCESSING', action: 'RETRY_ITEM_PROCESSING', icon: 'GET_ICON_RETRY', className: 'filepond--action-retry-item-processing', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, RevertItemProcessing: { label: 'GET_LABEL_BUTTON_UNDO_ITEM_PROCESSING', action: 'REQUEST_REVERT_ITEM_PROCESSING', icon: 'GET_ICON_UNDO', className: 'filepond--action-revert-item-processing', align: 'BUTTON_PROCESS_ITEM_POSITION', // right }, }; // make a list of buttons, we can then remove buttons from this list if they're disabled const ButtonKeys = []; forin(Buttons, key => { ButtonKeys.push(key); }); const calculateFileInfoOffset = root => { if (getRemoveIndicatorAligment(root) === 'right') return 0; const buttonRect = root.ref.buttonRemoveItem.rect.element; return buttonRect.hidden ? null : buttonRect.width + buttonRect.left; }; const calculateButtonWidth = root => { const buttonRect = root.ref.buttonAbortItemLoad.rect.element; return buttonRect.width; }; // Force on full pixels so text stays crips const calculateFileVerticalCenterOffset = root => Math.floor(root.ref.buttonRemoveItem.rect.element.height / 4); const calculateFileHorizontalCenterOffset = root => Math.floor(root.ref.buttonRemoveItem.rect.element.left / 2); const getLoadIndicatorAlignment = root => root.query('GET_STYLE_LOAD_INDICATOR_POSITION'); const getProcessIndicatorAlignment = root => root.query('GET_STYLE_PROGRESS_INDICATOR_POSITION'); const getRemoveIndicatorAligment = root => root.query('GET_STYLE_BUTTON_REMOVE_ITEM_POSITION'); const DefaultStyle = { buttonAbortItemLoad: { opacity: 0 }, buttonRetryItemLoad: { opacity: 0 }, buttonRemoveItem: { opacity: 0 }, buttonProcessItem: { opacity: 0 }, buttonAbortItemProcessing: { opacity: 0 }, buttonRetryItemProcessing: { opacity: 0 }, buttonRevertItemProcessing: { opacity: 0 }, loadProgressIndicator: { opacity: 0, align: getLoadIndicatorAlignment }, processProgressIndicator: { opacity: 0, align: getProcessIndicatorAlignment }, processingCompleteIndicator: { opacity: 0, scaleX: 0.75, scaleY: 0.75 }, info: { translateX: 0, translateY: 0, opacity: 0 }, status: { translateX: 0, translateY: 0, opacity: 0 }, }; const IdleStyle = { buttonRemoveItem: { opacity: 1 }, buttonProcessItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { translateX: calculateFileInfoOffset }, }; const ProcessingStyle = { buttonAbortItemProcessing: { opacity: 1 }, processProgressIndicator: { opacity: 1 }, status: { opacity: 1 }, }; const StyleMap = { DID_THROW_ITEM_INVALID: { buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { translateX: calculateFileInfoOffset, opacity: 1 }, }, DID_START_ITEM_LOAD: { buttonAbortItemLoad: { opacity: 1 }, loadProgressIndicator: { opacity: 1 }, status: { opacity: 1 }, }, DID_THROW_ITEM_LOAD_ERROR: { buttonRetryItemLoad: { opacity: 1 }, buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 1 }, }, DID_START_ITEM_REMOVE: { processProgressIndicator: { opacity: 1, align: getRemoveIndicatorAligment }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 0 }, }, DID_THROW_ITEM_REMOVE_ERROR: { processProgressIndicator: { opacity: 0, align: getRemoveIndicatorAligment }, buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 1, translateX: calculateFileInfoOffset }, }, DID_LOAD_ITEM: IdleStyle, DID_LOAD_LOCAL_ITEM: { buttonRemoveItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { translateX: calculateFileInfoOffset }, }, DID_START_ITEM_PROCESSING: ProcessingStyle, DID_REQUEST_ITEM_PROCESSING: ProcessingStyle, DID_UPDATE_ITEM_PROCESS_PROGRESS: ProcessingStyle, DID_COMPLETE_ITEM_PROCESSING: { buttonRevertItemProcessing: { opacity: 1 }, info: { opacity: 1 }, status: { opacity: 1 }, }, DID_THROW_ITEM_PROCESSING_ERROR: { buttonRemoveItem: { opacity: 1 }, buttonRetryItemProcessing: { opacity: 1 }, status: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, }, DID_THROW_ITEM_PROCESSING_REVERT_ERROR: { buttonRevertItemProcessing: { opacity: 1 }, status: { opacity: 1 }, info: { opacity: 1 }, }, DID_ABORT_ITEM_PROCESSING: { buttonRemoveItem: { opacity: 1 }, buttonProcessItem: { opacity: 1 }, info: { translateX: calculateFileInfoOffset }, status: { opacity: 1 }, }, DID_REVERT_ITEM_PROCESSING: IdleStyle, }; // complete indicator view const processingCompleteIndicatorView = createView({ create: ({ root }) => { root.element.innerHTML = root.query('GET_ICON_DONE'); }, name: 'processing-complete-indicator', ignoreRect: true, mixins: { styles: ['scaleX', 'scaleY', 'opacity'], animations: { scaleX: 'spring', scaleY: 'spring', opacity: { type: 'tween', duration: 250 }, }, }, }); /** * Creates the file view */ const create = ({ root, props }) => { // copy Buttons object const LocalButtons = Object.keys(Buttons).reduce((prev, curr) => { prev[curr] = { ...Buttons[curr] }; return prev; }, {}); const { id } = props; // allow reverting upload const allowRevert = root.query('GET_ALLOW_REVERT'); // allow remove file const allowRemove = root.query('GET_ALLOW_REMOVE'); // allow processing upload const allowProcess = root.query('GET_ALLOW_PROCESS'); // is instant uploading, need this to determine the icon of the undo button const instantUpload = root.query('GET_INSTANT_UPLOAD'); // is async set up const isAsync = root.query('IS_ASYNC'); // should align remove item buttons const alignRemoveItemButton = root.query('GET_STYLE_BUTTON_REMOVE_ITEM_ALIGN'); // enabled buttons array let buttonFilter; if (isAsync) { if (allowProcess && !allowRevert) { // only remove revert button buttonFilter = key => !/RevertItemProcessing/.test(key); } else if (!allowProcess && allowRevert) { // only remove process button buttonFilter = key => !/ProcessItem|RetryItemProcessing|AbortItemProcessing/.test(key); } else if (!allowProcess && !allowRevert) { // remove all process buttons buttonFilter = key => !/Process/.test(key); } } else { // no process controls available buttonFilter = key => !/Process/.test(key); } const enabledButtons = buttonFilter ? ButtonKeys.filter(buttonFilter) : ButtonKeys.concat(); // update icon and label for revert button when instant uploading if (instantUpload && allowRevert) { LocalButtons['RevertItemProcessing'].label = 'GET_LABEL_BUTTON_REMOVE_ITEM'; LocalButtons['RevertItemProcessing'].icon = 'GET_ICON_REMOVE'; } // remove last button (revert) if not allowed if (isAsync && !allowRevert) { const map = StyleMap['DID_COMPLETE_ITEM_PROCESSING']; map.info.translateX = calculateFileHorizontalCenterOffset; map.info.translateY = calculateFileVerticalCenterOffset; map.status.translateY = calculateFileVerticalCenterOffset; map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 }; } // should align center if (isAsync && !allowProcess) { [ 'DID_START_ITEM_PROCESSING', 'DID_REQUEST_ITEM_PROCESSING', 'DID_UPDATE_ITEM_PROCESS_PROGRESS', 'DID_THROW_ITEM_PROCESSING_ERROR', ].forEach(key => { StyleMap[key].status.translateY = calculateFileVerticalCenterOffset; }); StyleMap['DID_THROW_ITEM_PROCESSING_ERROR'].status.translateX = calculateButtonWidth; } // move remove button to right if (alignRemoveItemButton && allowRevert) { LocalButtons['RevertItemProcessing'].align = 'BUTTON_REMOVE_ITEM_POSITION'; const map = StyleMap['DID_COMPLETE_ITEM_PROCESSING']; map.info.translateX = calculateFileInfoOffset; map.status.translateY = calculateFileVerticalCenterOffset; map.processingCompleteIndicator = { opacity: 1, scaleX: 1, scaleY: 1 }; } // show/hide RemoveItem button if (!allowRemove) { LocalButtons['RemoveItem'].disabled = true; } // create the button views forin(LocalButtons, (key, definition) => { // create button const buttonView = root.createChildView(fileActionButton, { label: root.query(definition.label), icon: root.query(definition.icon), opacity: 0, }); // should be appended? if (enabledButtons.includes(key)) { root.appendChildView(buttonView); } // toggle if (definition.disabled) { buttonView.element.setAttribute('disabled', 'disabled'); buttonView.element.setAttribute('hidden', 'hidden'); } // add position attribute buttonView.element.dataset.align = root.query(`GET_STYLE_${definition.align}`); // add class buttonView.element.classList.add(definition.className); // handle interactions buttonView.on('click', e => { e.stopPropagation(); if (definition.disabled) return; root.dispatch(definition.action, { query: id }); }); // set reference root.ref[`button${key}`] = buttonView; }); // checkmark root.ref.processingCompleteIndicator = root.appendChildView( root.createChildView(processingCompleteIndicatorView) ); root.ref.processingCompleteIndicator.element.dataset.align = root.query( `GET_STYLE_BUTTON_PROCESS_ITEM_POSITION` ); // create file info view root.ref.info = root.appendChildView(root.createChildView(fileInfo, { id })); // create file status view root.ref.status = root.appendChildView(root.createChildView(fileStatus, { id })); // add progress indicators const loadIndicatorView = root.appendChildView( root.createChildView(progressIndicator, { opacity: 0, align: root.query(`GET_STYLE_LOAD_INDICATOR_POSITION`), }) ); loadIndicatorView.element.classList.add('filepond--load-indicator'); root.ref.loadProgressIndicator = loadIndicatorView; const progressIndicatorView = root.appendChildView( root.createChildView(progressIndicator, { opacity: 0, align: root.query(`GET_STYLE_PROGRESS_INDICATOR_POSITION`), }) ); progressIndicatorView.element.classList.add('filepond--process-indicator'); root.ref.processProgressIndicator = progressIndicatorView; // current active styles root.ref.activeStyles = []; }; const write = ({ root, actions, props }) => { // route actions route({ root, actions, props }); // select last state change action let action = actions .concat() .filter(action => /^DID_/.test(action.type)) .reverse() .find(action => StyleMap[action.type]); // a new action happened, let's get the matching styles if (action) { // define new active styles root.ref.activeStyles = []; const stylesToApply = StyleMap[action.type]; forin(DefaultStyle, (name, defaultStyles) => { // get reference to control const control = root.ref[name]; // loop over all styles for this control forin(defaultStyles, (key, defaultValue) => { const value = stylesToApply[name] && typeof stylesToApply[name][key] !== 'undefined' ? stylesToApply[name][key] : defaultValue; root.ref.activeStyles.push({ control, key, value }); }); }); } // apply active styles to element root.ref.activeStyles.forEach(({ control, key, value }) => { control[key] = typeof value === 'function' ? value(root) : value; }); }; const route = createRoute({ DID_SET_LABEL_BUTTON_ABORT_ITEM_PROCESSING: ({ root, action }) => { root.ref.buttonAbortItemProcessing.label = action.value; }, DID_SET_LABEL_BUTTON_ABORT_ITEM_LOAD: ({ root, action }) => { root.ref.buttonAbortItemLoad.label = action.value; }, DID_SET_LABEL_BUTTON_ABORT_ITEM_REMOVAL: ({ root, action }) => { root.ref.buttonAbortItemRemoval.label = action.value; }, DID_REQUEST_ITEM_PROCESSING: ({ root }) => { root.ref.processProgressIndicator.spin = true; root.ref.processProgressIndicator.progress = 0; }, DID_START_ITEM_LOAD: ({ root }) => { root.ref.loadProgressIndicator.spin = true; root.ref.loadProgressIndicator.progress = 0; }, DID_START_ITEM_REMOVE: ({ root }) => { root.ref.processProgressIndicator.spin = true; root.ref.processProgressIndicator.progress = 0; }, DID_UPDATE_ITEM_LOAD_PROGRESS: ({ root, action }) => { root.ref.loadProgressIndicator.spin = false; root.ref.loadProgressIndicator.progress = action.progress; }, DID_UPDATE_ITEM_PROCESS_PROGRESS: ({ root, action }) => { root.ref.processProgressIndicator.spin = false; root.ref.processProgressIndicator.progress = action.progress; }, }); export const file = createView({ create, write, didCreateView: root => { applyFilters('CREATE_VIEW', { ...root, view: root }); }, name: 'file', }); ================================================ FILE: src/js/app/view/fileActionButton.js ================================================ import { createView } from '../frame/index'; import { attr } from '../../utils/attr'; const create = ({ root, props }) => { root.element.innerHTML = (props.icon || '') + `${props.label}`; props.isDisabled = false; }; const write = ({ root, props }) => { const { isDisabled } = props; const shouldDisable = root.query('GET_DISABLED') || props.opacity === 0; if (shouldDisable && !isDisabled) { props.isDisabled = true; attr(root.element, 'disabled', 'disabled'); } else if (!shouldDisable && isDisabled) { props.isDisabled = false; root.element.removeAttribute('disabled'); } }; export const fileActionButton = createView({ tag: 'button', attributes: { type: 'button' }, ignoreRect: true, ignoreRectUpdate: true, name: 'file-action-button', mixins: { apis: ['label'], styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity'], animations: { scaleX: 'spring', scaleY: 'spring', translateX: 'spring', translateY: 'spring', opacity: { type: 'tween', duration: 250 } }, listeners: true }, create, write }); ================================================ FILE: src/js/app/view/fileInfo.js ================================================ import { createView, createRoute } from '../frame/index'; import { toNaturalFileSize } from '../../utils/toNaturalFileSize'; import { text } from '../../utils/text'; import { formatFilename } from '../../utils/formatFilename'; import { isInt } from '../../utils/isInt'; import { createElement } from '../../utils/createElement'; import { attr } from '../../utils/attr'; import { applyFilters } from '../../filter'; const create = ({ root, props }) => { // filename const fileName = createElement('span'); fileName.className = 'filepond--file-info-main'; // hide for screenreaders // the file is contained in a fieldset with legend that contains the filename // no need to read it twice attr(fileName, 'aria-hidden', 'true'); root.appendChild(fileName); root.ref.fileName = fileName; // filesize const fileSize = createElement('span'); fileSize.className = 'filepond--file-info-sub'; root.appendChild(fileSize); root.ref.fileSize = fileSize; // set initial values text(fileSize, root.query('GET_LABEL_FILE_WAITING_FOR_SIZE')); text(fileName, formatFilename(root.query('GET_ITEM_NAME', props.id))); }; const updateFile = ({ root, props }) => { text( root.ref.fileSize, toNaturalFileSize( root.query('GET_ITEM_SIZE', props.id), '.', root.query('GET_FILE_SIZE_BASE'), root.query('GET_FILE_SIZE_LABELS', root.query) ) ); text(root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id))); }; const updateFileSizeOnError = ({ root, props }) => { // if size is available don't fallback to unknown size message if (isInt(root.query('GET_ITEM_SIZE', props.id))) { updateFile({ root, props }); return; } text(root.ref.fileSize, root.query('GET_LABEL_FILE_SIZE_NOT_AVAILABLE')); }; export const fileInfo = createView({ name: 'file-info', ignoreRect: true, ignoreRectUpdate: true, write: createRoute({ DID_LOAD_ITEM: updateFile, DID_UPDATE_ITEM_META: updateFile, DID_THROW_ITEM_LOAD_ERROR: updateFileSizeOnError, DID_THROW_ITEM_INVALID: updateFileSizeOnError, }), didCreateView: root => { applyFilters('CREATE_VIEW', { ...root, view: root }); }, create, mixins: { styles: ['translateX', 'translateY'], animations: { translateX: 'spring', translateY: 'spring', }, }, }); ================================================ FILE: src/js/app/view/fileStatus.js ================================================ import { createView, createRoute } from '../frame/index'; import { toPercentage } from '../../utils/toPercentage'; import { text } from '../../utils/text'; import { createElement } from '../../utils/createElement'; import { applyFilters } from '../../filter'; const create = ({ root }) => { // main status const main = createElement('span'); main.className = 'filepond--file-status-main'; root.appendChild(main); root.ref.main = main; // sub status const sub = createElement('span'); sub.className = 'filepond--file-status-sub'; root.appendChild(sub); root.ref.sub = sub; didSetItemLoadProgress({ root, action: { progress: null } }); }; const didSetItemLoadProgress = ({ root, action }) => { const title = action.progress === null ? root.query('GET_LABEL_FILE_LOADING') : `${root.query('GET_LABEL_FILE_LOADING')} ${toPercentage( action.progress )}%`; text(root.ref.main, title); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL')); }; const didSetItemProcessProgress = ({ root, action }) => { const title = action.progress === null ? root.query('GET_LABEL_FILE_PROCESSING') : `${root.query('GET_LABEL_FILE_PROCESSING')} ${toPercentage( action.progress )}%`; text(root.ref.main, title); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL')); }; const didRequestItemProcessing = ({ root }) => { text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING')); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_CANCEL')); }; const didAbortItemProcessing = ({ root }) => { text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_ABORTED')); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_RETRY')); }; const didCompleteItemProcessing = ({ root }) => { text(root.ref.main, root.query('GET_LABEL_FILE_PROCESSING_COMPLETE')); text(root.ref.sub, root.query('GET_LABEL_TAP_TO_UNDO')); }; const clear = ({ root }) => { text(root.ref.main, ''); text(root.ref.sub, ''); }; const error = ({ root, action }) => { text(root.ref.main, action.status.main); text(root.ref.sub, action.status.sub); }; export const fileStatus = createView({ name: 'file-status', ignoreRect: true, ignoreRectUpdate: true, write: createRoute({ DID_LOAD_ITEM: clear, DID_REVERT_ITEM_PROCESSING: clear, DID_REQUEST_ITEM_PROCESSING: didRequestItemProcessing, DID_ABORT_ITEM_PROCESSING: didAbortItemProcessing, DID_COMPLETE_ITEM_PROCESSING: didCompleteItemProcessing, DID_UPDATE_ITEM_PROCESS_PROGRESS: didSetItemProcessProgress, DID_UPDATE_ITEM_LOAD_PROGRESS: didSetItemLoadProgress, DID_THROW_ITEM_LOAD_ERROR: error, DID_THROW_ITEM_INVALID: error, DID_THROW_ITEM_PROCESSING_ERROR: error, DID_THROW_ITEM_PROCESSING_REVERT_ERROR: error, DID_THROW_ITEM_REMOVE_ERROR: error }), didCreateView: root => { applyFilters('CREATE_VIEW', { ...root, view: root }); }, create, mixins: { styles: ['translateX', 'translateY', 'opacity'], animations: { opacity: { type: 'tween', duration: 250 }, translateX: 'spring', translateY: 'spring' } } }); ================================================ FILE: src/js/app/view/fileWrapper.js ================================================ import { createView, createRoute } from '../frame/index'; import { formatFilename } from '../../utils/formatFilename'; import { createElement } from '../../utils/createElement'; import { text } from '../../utils/text'; import { file } from './file'; import { applyFilters } from '../../filter'; /** * Creates the file view */ const create = ({ root, props }) => { // filename root.ref.fileName = createElement('legend'); root.appendChild(root.ref.fileName); // file appended root.ref.file = root.appendChildView( root.createChildView(file, { id: props.id }) ); // data has moved to data.js root.ref.data = false; }; /** * Data storage */ const didLoadItem = ({ root, props }) => { // updates the legend of the fieldset so screenreaders can better group buttons text( root.ref.fileName, formatFilename(root.query('GET_ITEM_NAME', props.id)) ); }; export const fileWrapper = createView({ create, ignoreRect: true, write: createRoute({ DID_LOAD_ITEM: didLoadItem }), didCreateView: root => { applyFilters('CREATE_VIEW', { ...root, view: root }); }, tag: 'fieldset', name: 'file-wrapper' }); ================================================ FILE: src/js/app/view/item.js ================================================ import { createView, createRoute } from '../frame/index'; import { fileWrapper } from './fileWrapper'; import { panel } from './panel'; import { createDragHelper } from '../utils/createDragHelper'; const ITEM_TRANSLATE_SPRING = { type: 'spring', stiffness: 0.75, damping: 0.45, mass: 10, }; const ITEM_SCALE_SPRING = 'spring'; const StateMap = { DID_START_ITEM_LOAD: 'busy', DID_UPDATE_ITEM_LOAD_PROGRESS: 'loading', DID_THROW_ITEM_INVALID: 'load-invalid', DID_THROW_ITEM_LOAD_ERROR: 'load-error', DID_LOAD_ITEM: 'idle', DID_THROW_ITEM_REMOVE_ERROR: 'remove-error', DID_START_ITEM_REMOVE: 'busy', DID_START_ITEM_PROCESSING: 'busy processing', DID_REQUEST_ITEM_PROCESSING: 'busy processing', DID_UPDATE_ITEM_PROCESS_PROGRESS: 'processing', DID_COMPLETE_ITEM_PROCESSING: 'processing-complete', DID_THROW_ITEM_PROCESSING_ERROR: 'processing-error', DID_THROW_ITEM_PROCESSING_REVERT_ERROR: 'processing-revert-error', DID_ABORT_ITEM_PROCESSING: 'cancelled', DID_REVERT_ITEM_PROCESSING: 'idle', }; /** * Creates the file view */ const create = ({ root, props }) => { // select root.ref.handleClick = e => root.dispatch('DID_ACTIVATE_ITEM', { id: props.id }); // set id root.element.id = `filepond--item-${props.id}`; root.element.addEventListener('click', root.ref.handleClick); // file view root.ref.container = root.appendChildView(root.createChildView(fileWrapper, { id: props.id })); // file panel root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'item-panel' })); // default start height root.ref.panel.height = null; // by default not marked for removal props.markedForRemoval = false; // if not allowed to reorder file items, exit here if (!root.query('GET_ALLOW_REORDER')) return; // set to idle so shows grab cursor root.element.dataset.dragState = 'idle'; const grab = e => { if (!e.isPrimary) return; let removedActivateListener = false; const origin = { x: e.pageX, y: e.pageY, }; props.dragOrigin = { x: root.translateX, y: root.translateY, }; props.dragCenter = { x: e.offsetX, y: e.offsetY, }; const dragState = createDragHelper(root.query('GET_ACTIVE_ITEMS')); root.dispatch('DID_GRAB_ITEM', { id: props.id, dragState }); const drag = e => { if (!e.isPrimary) return; e.stopPropagation(); e.preventDefault(); props.dragOffset = { x: e.pageX - origin.x, y: e.pageY - origin.y, }; // if dragged stop listening to clicks, will re-add when done dragging const dist = props.dragOffset.x * props.dragOffset.x + props.dragOffset.y * props.dragOffset.y; if (dist > 16 && !removedActivateListener) { removedActivateListener = true; root.element.removeEventListener('click', root.ref.handleClick); } root.dispatch('DID_DRAG_ITEM', { id: props.id, dragState }); }; const drop = e => { if (!e.isPrimary) return; props.dragOffset = { x: e.pageX - origin.x, y: e.pageY - origin.y, }; reset(); }; const cancel = () => { reset(); }; const reset = () => { document.removeEventListener('pointercancel', cancel); document.removeEventListener('pointermove', drag); document.removeEventListener('pointerup', drop); root.dispatch('DID_DROP_ITEM', { id: props.id, dragState }); // start listening to clicks again if (removedActivateListener) { setTimeout(() => root.element.addEventListener('click', root.ref.handleClick), 0); } }; document.addEventListener('pointercancel', cancel); document.addEventListener('pointermove', drag); document.addEventListener('pointerup', drop); }; root.element.addEventListener('pointerdown', grab); }; const route = createRoute({ DID_UPDATE_PANEL_HEIGHT: ({ root, action }) => { root.height = action.height; }, }); const write = createRoute( { DID_GRAB_ITEM: ({ root, props }) => { props.dragOrigin = { x: root.translateX, y: root.translateY, }; }, DID_DRAG_ITEM: ({ root }) => { root.element.dataset.dragState = 'drag'; }, DID_DROP_ITEM: ({ root, props }) => { props.dragOffset = null; props.dragOrigin = null; root.element.dataset.dragState = 'drop'; }, }, ({ root, actions, props, shouldOptimize }) => { if (root.element.dataset.dragState === 'drop') { if (root.scaleX <= 1) { root.element.dataset.dragState = 'idle'; } } // select last state change action let action = actions .concat() .filter(action => /^DID_/.test(action.type)) .reverse() .find(action => StateMap[action.type]); // no need to set same state twice if (action && action.type !== props.currentState) { // set current state props.currentState = action.type; // set state root.element.dataset.filepondItemState = StateMap[props.currentState] || ''; } // route actions const aspectRatio = root.query('GET_ITEM_PANEL_ASPECT_RATIO') || root.query('GET_PANEL_ASPECT_RATIO'); if (!aspectRatio) { route({ root, actions, props }); if (!root.height && root.ref.container.rect.element.height > 0) { root.height = root.ref.container.rect.element.height; } } else if (!shouldOptimize) { root.height = root.rect.element.width * aspectRatio; } // sync panel height with item height if (shouldOptimize) { root.ref.panel.height = null; } root.ref.panel.height = root.height; } ); export const item = createView({ create, write, destroy: ({ root, props }) => { root.element.removeEventListener('click', root.ref.handleClick); root.dispatch('RELEASE_ITEM', { query: props.id }); }, tag: 'li', name: 'item', mixins: { apis: [ 'id', 'interactionMethod', 'markedForRemoval', 'spawnDate', 'dragCenter', 'dragOrigin', 'dragOffset', ], styles: ['translateX', 'translateY', 'scaleX', 'scaleY', 'opacity', 'height'], animations: { scaleX: ITEM_SCALE_SPRING, scaleY: ITEM_SCALE_SPRING, translateX: ITEM_TRANSLATE_SPRING, translateY: ITEM_TRANSLATE_SPRING, opacity: { type: 'tween', duration: 150 }, }, }, }); ================================================ FILE: src/js/app/view/list.js ================================================ import { createView, createRoute } from '../frame/index'; import { InteractionMethod } from '../enum/InteractionMethod'; import { item } from './item'; import { attr } from '../../utils/attr'; import { getItemIndexByPosition } from '../utils/getItemIndexByPosition'; import { dropAreaDimensions } from '../utils/dropAreaDimensions'; import getItemsPerRow from '../utils/getItemsPerRow'; const create = ({ root }) => { // need to set role to list as otherwise it won't be read as a list by VoiceOver attr(root.element, 'role', 'list'); root.ref.lastItemSpanwDate = Date.now(); }; /** * Inserts a new item * @param root * @param action */ const addItemView = ({ root, action }) => { const { id, index, interactionMethod } = action; root.ref.addIndex = index; const now = Date.now(); let spawnDate = now; let opacity = 1; if (interactionMethod !== InteractionMethod.NONE) { opacity = 0; const cooldown = root.query('GET_ITEM_INSERT_INTERVAL'); const dist = now - root.ref.lastItemSpanwDate; spawnDate = dist < cooldown ? now + (cooldown - dist) : now; } root.ref.lastItemSpanwDate = spawnDate; root.appendChildView( root.createChildView( // view type item, // props { spawnDate, id, opacity, interactionMethod, } ), index ); }; const moveItem = (item, x, y, vx = 0, vy = 1) => { // set to null to remove animation while dragging if (item.dragOffset) { item.translateX = null; item.translateY = null; item.translateX = item.dragOrigin.x + item.dragOffset.x; item.translateY = item.dragOrigin.y + item.dragOffset.y; item.scaleX = 1.025; item.scaleY = 1.025; } else { item.translateX = x; item.translateY = y; if (Date.now() > item.spawnDate) { // reveal element if (item.opacity === 0) { introItemView(item, x, y, vx, vy); } // make sure is default scale every frame item.scaleX = 1; item.scaleY = 1; item.opacity = 1; } } }; const introItemView = (item, x, y, vx, vy) => { if (item.interactionMethod === InteractionMethod.NONE) { item.translateX = null; item.translateX = x; item.translateY = null; item.translateY = y; } else if (item.interactionMethod === InteractionMethod.DROP) { item.translateX = null; item.translateX = x - vx * 20; item.translateY = null; item.translateY = y - vy * 10; item.scaleX = 0.8; item.scaleY = 0.8; } else if (item.interactionMethod === InteractionMethod.BROWSE) { item.translateY = null; item.translateY = y - 30; } else if (item.interactionMethod === InteractionMethod.API) { item.translateX = null; item.translateX = x - 30; item.translateY = null; } }; /** * Removes an existing item * @param root * @param action */ const removeItemView = ({ root, action }) => { const { id } = action; // get the view matching the given id const view = root.childViews.find(child => child.id === id); // if no view found, exit if (!view) { return; } // animate view out of view view.scaleX = 0.9; view.scaleY = 0.9; view.opacity = 0; // mark for removal view.markedForRemoval = true; }; const getItemHeight = child => child.rect.element.height + child.rect.element.marginBottom + child.rect.element.marginTop; const getItemWidth = child => child.rect.element.width + child.rect.element.marginLeft * 0.5 + child.rect.element.marginRight * 0.5; const dragItem = ({ root, action }) => { const { id, dragState } = action; // reference to item const item = root.query('GET_ITEM', { id }); // get the view matching the given id const view = root.childViews.find(child => child.id === id); const numItems = root.childViews.length; const oldIndex = dragState.getItemIndex(item); // if no view found, exit if (!view) return; const dragPosition = { x: view.dragOrigin.x + view.dragOffset.x + view.dragCenter.x, y: view.dragOrigin.y + view.dragOffset.y + view.dragCenter.y, }; // get drag area dimensions const dragHeight = getItemHeight(view); const dragWidth = getItemWidth(view); // get rows and columns (There will always be at least one row and one column if a file is present) let cols = Math.floor(root.rect.outer.width / dragWidth); if (cols > numItems) cols = numItems; // rows are used to find when we have left the preview area bounding box const rows = Math.floor(numItems / cols + 1); dropAreaDimensions.setHeight = dragHeight * rows; dropAreaDimensions.setWidth = dragWidth * cols; // get new index of dragged item var location = { y: Math.floor(dragPosition.y / dragHeight), x: Math.floor(dragPosition.x / dragWidth), getGridIndex: function getGridIndex() { if ( dragPosition.y > dropAreaDimensions.getHeight || dragPosition.y < 0 || dragPosition.x > dropAreaDimensions.getWidth || dragPosition.x < 0 ) return oldIndex; return this.y * cols + this.x; }, getColIndex: function getColIndex() { const items = root.query('GET_ACTIVE_ITEMS'); const visibleChildren = root.childViews.filter(child => child.rect.element.height); const children = items.map(item => visibleChildren.find(childView => childView.id === item.id) ); const currentIndex = children.findIndex(child => child === view); const dragHeight = getItemHeight(view); const l = children.length; let idx = l; let childHeight = 0; let childBottom = 0; let childTop = 0; for (let i = 0; i < l; i++) { childHeight = getItemHeight(children[i]); childTop = childBottom; childBottom = childTop + childHeight; if (dragPosition.y < childBottom) { if (currentIndex > i) { if (dragPosition.y < childTop + dragHeight) { idx = i; break; } continue; } idx = i; break; } } return idx; }, }; // get new index const index = cols > 1 ? location.getGridIndex() : location.getColIndex(); root.dispatch('MOVE_ITEM', { query: view, index }); // if the index of the item changed, dispatch reorder action const currentIndex = dragState.getIndex(); if (currentIndex === undefined || currentIndex !== index) { dragState.setIndex(index); if (currentIndex === undefined) return; root.dispatch('DID_REORDER_ITEMS', { items: root.query('GET_ACTIVE_ITEMS'), origin: oldIndex, target: index, }); } }; /** * Setup action routes */ const route = createRoute({ DID_ADD_ITEM: addItemView, DID_REMOVE_ITEM: removeItemView, DID_DRAG_ITEM: dragItem, }); /** * Write to view * @param root * @param actions * @param props */ const write = ({ root, props, actions, shouldOptimize }) => { // route actions route({ root, props, actions }); const { dragCoordinates } = props; // available space on horizontal axis const horizontalSpace = root.rect.element.width; // only draw children that have dimensions const visibleChildren = root.childViews.filter(child => child.rect.element.height); // sort based on current active items const children = root .query('GET_ACTIVE_ITEMS') .map(item => visibleChildren.find(child => child.id === item.id)) .filter(item => item); // get index const dragIndex = dragCoordinates ? getItemIndexByPosition(root, children, dragCoordinates) : null; // add index is used to reserve the dropped/added item index till the actual item is rendered const addIndex = root.ref.addIndex || null; // add index no longer needed till possibly next draw root.ref.addIndex = null; let dragIndexOffset = 0; let removeIndexOffset = 0; let addIndexOffset = 0; if (children.length === 0) return; const childRect = children[0].rect.element; const itemVerticalMargin = childRect.marginTop + childRect.marginBottom; const itemHorizontalMargin = childRect.marginLeft + childRect.marginRight; const itemWidth = childRect.width + itemHorizontalMargin; const itemHeight = childRect.height + itemVerticalMargin; const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth); // stack if (itemsPerRow === 1) { let offsetY = 0; let dragOffset = 0; children.forEach((child, index) => { if (dragIndex) { let dist = index - dragIndex; if (dist === -2) { dragOffset = -itemVerticalMargin * 0.25; } else if (dist === -1) { dragOffset = -itemVerticalMargin * 0.75; } else if (dist === 0) { dragOffset = itemVerticalMargin * 0.75; } else if (dist === 1) { dragOffset = itemVerticalMargin * 0.25; } else { dragOffset = 0; } } if (shouldOptimize) { child.translateX = null; child.translateY = null; } if (!child.markedForRemoval) { moveItem(child, 0, offsetY + dragOffset); } let itemHeight = child.rect.element.height + itemVerticalMargin; let visualHeight = itemHeight * (child.markedForRemoval ? child.opacity : 1); offsetY += visualHeight; }); } // grid else { let prevX = 0; let prevY = 0; children.forEach((child, index) => { if (index === dragIndex) { dragIndexOffset = 1; } if (index === addIndex) { addIndexOffset += 1; } if (child.markedForRemoval && child.opacity < 0.5) { removeIndexOffset -= 1; } const visualIndex = index + addIndexOffset + dragIndexOffset + removeIndexOffset; const indexX = visualIndex % itemsPerRow; const indexY = Math.floor(visualIndex / itemsPerRow); const offsetX = indexX * itemWidth; const offsetY = indexY * itemHeight; const vectorX = Math.sign(offsetX - prevX); const vectorY = Math.sign(offsetY - prevY); prevX = offsetX; prevY = offsetY; if (child.markedForRemoval) return; if (shouldOptimize) { child.translateX = null; child.translateY = null; } moveItem(child, offsetX, offsetY, vectorX, vectorY); }); } }; /** * Filters actions that are meant specifically for a certain child of the list * @param child * @param actions */ const filterSetItemActions = (child, actions) => actions.filter(action => { // if action has an id, filter out actions that don't have this child id if (action.data && action.data.id) { return child.id === action.data.id; } // allow all other actions return true; }); export const list = createView({ create, write, tag: 'ul', name: 'list', didWriteView: ({ root }) => { root.childViews .filter(view => view.markedForRemoval && view.opacity === 0 && view.resting) .forEach(view => { view._destroy(); root.removeChildView(view); }); }, filterFrameActionsForChild: filterSetItemActions, mixins: { apis: ['dragCoordinates'], }, }); ================================================ FILE: src/js/app/view/listScroller.js ================================================ import { createView, createRoute } from '../frame/index'; import { list } from './list'; const create = ({ root, props }) => { root.ref.list = root.appendChildView(root.createChildView(list)); props.dragCoordinates = null; props.overflowing = false; }; const storeDragCoordinates = ({ root, props, action }) => { if (!root.query('GET_ITEM_INSERT_LOCATION_FREEDOM')) return; props.dragCoordinates = { left: action.position.scopeLeft - root.ref.list.rect.element.left, top: action.position.scopeTop - (root.rect.outer.top + root.rect.element.marginTop + root.rect.element.scrollTop) }; }; const clearDragCoordinates = ({ props }) => { props.dragCoordinates = null; }; const route = createRoute({ DID_DRAG: storeDragCoordinates, DID_END_DRAG: clearDragCoordinates }); const write = ({ root, props, actions }) => { // route actions route({ root, props, actions }); // current drag position root.ref.list.dragCoordinates = props.dragCoordinates; // if currently overflowing but no longer received overflow if (props.overflowing && !props.overflow) { props.overflowing = false; // reset overflow state root.element.dataset.state = ''; root.height = null; } // if is not overflowing currently but does receive overflow value if (props.overflow) { const newHeight = Math.round(props.overflow); if (newHeight !== root.height) { props.overflowing = true; root.element.dataset.state = 'overflow'; root.height = newHeight; } } }; export const listScroller = createView({ create, write, name: 'list-scroller', mixins: { apis: ['overflow' , 'dragCoordinates'], styles: ['height', 'translateY'], animations: { translateY: 'spring' } } }); ================================================ FILE: src/js/app/view/panel.js ================================================ import { createView } from '../frame/index'; import { isBoolean } from '../../utils/isBoolean'; const PANEL_SPRING_PROPS = { type: 'spring', damping: 0.6, mass: 7 }; const create = ({ root, props }) => { [ { name: 'top' }, { name: 'center', props: { translateY: null, scaleY: null }, mixins: { animations: { scaleY: PANEL_SPRING_PROPS }, styles: ['translateY', 'scaleY'] } }, { name: 'bottom', props: { translateY: null }, mixins: { animations: { translateY: PANEL_SPRING_PROPS }, styles: ['translateY'] } } ].forEach(section => { createSection(root, section, props.name); }); root.element.classList.add(`filepond--${props.name}`); root.ref.scalable = null; }; const createSection = (root, section, className) => { const viewConstructor = createView({ name: `panel-${section.name} filepond--${className}`, mixins: section.mixins, ignoreRectUpdate: true }); const view = root.createChildView(viewConstructor, section.props); root.ref[section.name] = root.appendChildView(view); }; const write = ({ root, props }) => { // update scalable state if (root.ref.scalable === null || props.scalable !== root.ref.scalable) { root.ref.scalable = isBoolean(props.scalable) ? props.scalable : true; root.element.dataset.scalable = root.ref.scalable; } // no height, can't set if (!props.height) return; // get child rects const topRect = root.ref.top.rect.element; const bottomRect = root.ref.bottom.rect.element; // make sure height never is smaller than bottom and top seciton heights combined (will probably never happen, but who knows) const height = Math.max(topRect.height + bottomRect.height, props.height); // offset center part root.ref.center.translateY = topRect.height; // scale center part // use math ceil to prevent transparent lines because of rounding errors root.ref.center.scaleY = (height - topRect.height - bottomRect.height) / 100; // offset bottom part root.ref.bottom.translateY = height - bottomRect.height; }; export const panel = createView({ name: 'panel', read: ({ root, props }) => props.heightCurrent = root.ref.bottom.translateY, write, create, ignoreRect: true, mixins: { apis: ['height', 'heightCurrent', 'scalable'] } }); ================================================ FILE: src/js/app/view/progressIndicator.js ================================================ import { createView } from '../frame/index'; import { createElement } from '../frame/utils/createElement'; import { attr } from '../../utils/attr'; import { percentageArc } from '../../utils/percentageArc'; const create = ({ root, props }) => { // start at 0 props.spin = false; props.progress = 0; props.opacity = 0; // svg const svg = createElement('svg'); root.ref.path = createElement('path', { 'stroke-width': 2, 'stroke-linecap': 'round', }); svg.appendChild(root.ref.path); root.ref.svg = svg; root.appendChild(svg); }; const write = ({ root, props }) => { if (props.opacity === 0) { return; } if (props.align) { root.element.dataset.align = props.align; } // get width of stroke const ringStrokeWidth = parseInt(attr(root.ref.path, 'stroke-width'), 10); // calculate size of ring const size = root.rect.element.width * 0.5; // ring state let ringFrom = 0; let ringTo = 0; // now in busy mode if (props.spin) { ringFrom = 0; ringTo = 0.5; } else { ringFrom = 0; ringTo = props.progress; } // get arc path const coordinates = percentageArc(size, size, size - ringStrokeWidth, ringFrom, ringTo); // update progress bar attr(root.ref.path, 'd', coordinates); // hide while contains 0 value attr(root.ref.path, 'stroke-opacity', props.spin || props.progress > 0 ? 1 : 0); }; export const progressIndicator = createView({ tag: 'div', name: 'progress-indicator', ignoreRectUpdate: true, ignoreRect: true, create, write, mixins: { apis: ['progress', 'spin', 'align'], styles: ['opacity'], animations: { opacity: { type: 'tween', duration: 500 }, progress: { type: 'spring', stiffness: 0.95, damping: 0.65, mass: 10, }, }, }, }); ================================================ FILE: src/js/app/view/root.js ================================================ import { createView, createRoute } from '../frame/index'; import { applyFilterChain, applyFilters } from '../../filter'; import { listScroller } from './listScroller'; import { panel } from './panel'; import { browser } from './browser'; import { dropLabel } from './dropLabel'; import { drip } from './drip'; import { data } from './data'; import { createHopper } from '../utils/createHopper'; import { createPaster } from '../utils/createPaster'; import { InteractionMethod } from '../enum/InteractionMethod'; import { getItemIndexByPosition } from '../utils/getItemIndexByPosition'; import { isInt } from '../../utils/isInt'; import { isEmpty } from '../../utils/isEmpty'; import { assistant } from './assistant'; import { toCamels } from '../../utils/toCamels'; import { createElement } from '../../utils/createElement'; import { createResponse } from '../../utils/createResponse'; import { debounce } from '../../utils/debounce'; import { isFile } from '../../utils/isFile'; import getItemsPerRow from '../utils/getItemsPerRow'; const MAX_FILES_LIMIT = 1000000; const prevent = e => e.preventDefault(); const create = ({ root, props }) => { // Add id const id = root.query('GET_ID'); if (id) { root.element.id = id; } // Add className const className = root.query('GET_CLASS_NAME'); if (className) { className .split(' ') .filter(name => name.length) .forEach(name => { root.element.classList.add(name); }); } // Field label root.ref.label = root.appendChildView( root.createChildView(dropLabel, { ...props, translateY: null, caption: root.query('GET_LABEL_IDLE'), }) ); // List of items root.ref.list = root.appendChildView(root.createChildView(listScroller, { translateY: null })); // Background panel root.ref.panel = root.appendChildView(root.createChildView(panel, { name: 'panel-root' })); // Assistant notifies assistive tech when content changes root.ref.assistant = root.appendChildView(root.createChildView(assistant, { ...props })); // Data root.ref.data = root.appendChildView(root.createChildView(data, { ...props })); // Measure (tests if fixed height was set) // DOCTYPE needs to be set for this to work root.ref.measure = createElement('div'); root.ref.measure.style.height = '100%'; root.element.appendChild(root.ref.measure); // information on the root height or fixed height status root.ref.bounds = null; // apply initial style properties root.query('GET_STYLES') .filter(style => !isEmpty(style.value)) .map(({ name, value }) => { root.element.dataset[name] = value; }); // determine if width changed root.ref.widthPrevious = null; root.ref.widthUpdated = debounce(() => { root.ref.updateHistory = []; root.dispatch('DID_RESIZE_ROOT'); }, 250); // history of updates root.ref.previousAspectRatio = null; root.ref.updateHistory = []; // prevent scrolling and zooming on iOS (only if supports pointer events, for then we can enable reorder) const canHover = window.matchMedia('(pointer: fine) and (hover: hover)').matches; const hasPointerEvents = 'PointerEvent' in window; if (root.query('GET_ALLOW_REORDER') && hasPointerEvents && !canHover) { root.element.addEventListener('touchmove', prevent, { passive: false }); root.element.addEventListener('gesturestart', prevent); } // add credits const credits = root.query('GET_CREDITS'); const hasCredits = credits.length === 2; if (hasCredits) { const frag = document.createElement('a'); frag.className = 'filepond--credits'; frag.href = credits[0]; frag.tabIndex = -1; frag.target = '_blank'; frag.rel = 'noopener noreferrer nofollow'; frag.textContent = credits[1]; root.element.appendChild(frag); root.ref.credits = frag; } }; const write = ({ root, props, actions }) => { // route actions route({ root, props, actions }); // apply style properties actions .filter(action => /^DID_SET_STYLE_/.test(action.type)) .filter(action => !isEmpty(action.data.value)) .map(({ type, data }) => { const name = toCamels(type.substring(8).toLowerCase(), '_'); root.element.dataset[name] = data.value; root.invalidateLayout(); }); if (root.rect.element.hidden) return; if (root.rect.element.width !== root.ref.widthPrevious) { root.ref.widthPrevious = root.rect.element.width; root.ref.widthUpdated(); } // get box bounds, we do this only once let bounds = root.ref.bounds; if (!bounds) { bounds = root.ref.bounds = calculateRootBoundingBoxHeight(root); // destroy measure element root.element.removeChild(root.ref.measure); root.ref.measure = null; } // get quick references to various high level parts of the upload tool const { hopper, label, list, panel } = root.ref; // sets correct state to hopper scope if (hopper) { hopper.updateHopperState(); } // bool to indicate if we're full or not const aspectRatio = root.query('GET_PANEL_ASPECT_RATIO'); const isMultiItem = root.query('GET_ALLOW_MULTIPLE'); const totalItems = root.query('GET_TOTAL_ITEMS'); const maxItems = isMultiItem ? root.query('GET_MAX_FILES') || MAX_FILES_LIMIT : 1; const atMaxCapacity = totalItems === maxItems; // action used to add item const addAction = actions.find(action => action.type === 'DID_ADD_ITEM'); // if reached max capacity and we've just reached it if (atMaxCapacity && addAction) { // get interaction type const interactionMethod = addAction.data.interactionMethod; // hide label label.opacity = 0; if (isMultiItem) { label.translateY = -40; } else { if (interactionMethod === InteractionMethod.API) { label.translateX = 40; } else if (interactionMethod === InteractionMethod.BROWSE) { label.translateY = 40; } else { label.translateY = 30; } } } else if (!atMaxCapacity) { label.opacity = 1; label.translateX = 0; label.translateY = 0; } const listItemMargin = calculateListItemMargin(root); const listHeight = calculateListHeight(root); const labelHeight = label.rect.element.height; const currentLabelHeight = !isMultiItem || atMaxCapacity ? 0 : labelHeight; const listMarginTop = atMaxCapacity ? list.rect.element.marginTop : 0; const listMarginBottom = totalItems === 0 ? 0 : list.rect.element.marginBottom; const visualHeight = currentLabelHeight + listMarginTop + listHeight.visual + listMarginBottom; const boundsHeight = currentLabelHeight + listMarginTop + listHeight.bounds + listMarginBottom; // link list to label bottom position list.translateY = Math.max(0, currentLabelHeight - list.rect.element.marginTop) - listItemMargin.top; if (aspectRatio) { // fixed aspect ratio // calculate height based on width const width = root.rect.element.width; const height = width * aspectRatio; // clear history if aspect ratio has changed if (aspectRatio !== root.ref.previousAspectRatio) { root.ref.previousAspectRatio = aspectRatio; root.ref.updateHistory = []; } // remember this width const history = root.ref.updateHistory; history.push(width); const MAX_BOUNCES = 2; if (history.length > MAX_BOUNCES * 2) { const l = history.length; const bottom = l - 10; let bounces = 0; for (let i = l; i >= bottom; i--) { if (history[i] === history[i - 2]) { bounces++; } if (bounces >= MAX_BOUNCES) { // dont adjust height return; } } } // fix height of panel so it adheres to aspect ratio panel.scalable = false; panel.height = height; // available height for list const listAvailableHeight = // the height of the panel minus the label height height - currentLabelHeight - // the room we leave open between the end of the list and the panel bottom (listMarginBottom - listItemMargin.bottom) - // if we're full we need to leave some room between the top of the panel and the list (atMaxCapacity ? listMarginTop : 0); if (listHeight.visual > listAvailableHeight) { list.overflow = listAvailableHeight; } else { list.overflow = null; } // set container bounds (so pushes siblings downwards) root.height = height; } else if (bounds.fixedHeight) { // fixed height // fix height of panel panel.scalable = false; // available height for list const listAvailableHeight = // the height of the panel minus the label height bounds.fixedHeight - currentLabelHeight - // the room we leave open between the end of the list and the panel bottom (listMarginBottom - listItemMargin.bottom) - // if we're full we need to leave some room between the top of the panel and the list (atMaxCapacity ? listMarginTop : 0); // set list height if (listHeight.visual > listAvailableHeight) { list.overflow = listAvailableHeight; } else { list.overflow = null; } // no need to set container bounds as these are handles by CSS fixed height } else if (bounds.cappedHeight) { // max-height // not a fixed height panel const isCappedHeight = visualHeight >= bounds.cappedHeight; const panelHeight = Math.min(bounds.cappedHeight, visualHeight); panel.scalable = true; panel.height = isCappedHeight ? panelHeight : panelHeight - listItemMargin.top - listItemMargin.bottom; // available height for list const listAvailableHeight = // the height of the panel minus the label height panelHeight - currentLabelHeight - // the room we leave open between the end of the list and the panel bottom (listMarginBottom - listItemMargin.bottom) - // if we're full we need to leave some room between the top of the panel and the list (atMaxCapacity ? listMarginTop : 0); // set list height (if is overflowing) if (visualHeight > bounds.cappedHeight && listHeight.visual > listAvailableHeight) { list.overflow = listAvailableHeight; } else { list.overflow = null; } // set container bounds (so pushes siblings downwards) root.height = Math.min( bounds.cappedHeight, boundsHeight - listItemMargin.top - listItemMargin.bottom ); } else { // flexible height // not a fixed height panel const itemMargin = totalItems > 0 ? listItemMargin.top + listItemMargin.bottom : 0; panel.scalable = true; panel.height = Math.max(labelHeight, visualHeight - itemMargin); // set container bounds (so pushes siblings downwards) root.height = Math.max(labelHeight, boundsHeight - itemMargin); } // move credits to bottom if (root.ref.credits && panel.heightCurrent) root.ref.credits.style.transform = `translateY(${panel.heightCurrent}px)`; }; const calculateListItemMargin = root => { const item = root.ref.list.childViews[0].childViews[0]; return item ? { top: item.rect.element.marginTop, bottom: item.rect.element.marginBottom, } : { top: 0, bottom: 0, }; }; const calculateListHeight = root => { let visual = 0; let bounds = 0; // get file list reference const scrollList = root.ref.list; const itemList = scrollList.childViews[0]; const visibleChildren = itemList.childViews.filter(child => child.rect.element.height); const children = root .query('GET_ACTIVE_ITEMS') .map(item => visibleChildren.find(child => child.id === item.id)) .filter(item => item); // no children, done! if (children.length === 0) return { visual, bounds }; const horizontalSpace = itemList.rect.element.width; const dragIndex = getItemIndexByPosition(itemList, children, scrollList.dragCoordinates); const childRect = children[0].rect.element; const itemVerticalMargin = childRect.marginTop + childRect.marginBottom; const itemHorizontalMargin = childRect.marginLeft + childRect.marginRight; const itemWidth = childRect.width + itemHorizontalMargin; const itemHeight = childRect.height + itemVerticalMargin; const newItem = typeof dragIndex !== 'undefined' && dragIndex >= 0 ? 1 : 0; const removedItem = children.find(child => child.markedForRemoval && child.opacity < 0.45) ? -1 : 0; const verticalItemCount = children.length + newItem + removedItem; const itemsPerRow = getItemsPerRow(horizontalSpace, itemWidth); // stack if (itemsPerRow === 1) { children.forEach(item => { const height = item.rect.element.height + itemVerticalMargin; bounds += height; visual += height * item.opacity; }); } // grid else { bounds = Math.ceil(verticalItemCount / itemsPerRow) * itemHeight; visual = bounds; } return { visual, bounds }; }; const calculateRootBoundingBoxHeight = root => { const height = root.ref.measureHeight || null; const cappedHeight = parseInt(root.style.maxHeight, 10) || null; const fixedHeight = height === 0 ? null : height; return { cappedHeight, fixedHeight, }; }; const exceedsMaxFiles = (root, items) => { const allowReplace = root.query('GET_ALLOW_REPLACE'); const allowMultiple = root.query('GET_ALLOW_MULTIPLE'); const totalItems = root.query('GET_TOTAL_ITEMS'); let maxItems = root.query('GET_MAX_FILES'); // total amount of items being dragged const totalBrowseItems = items.length; // if does not allow multiple items and dragging more than one item if (!allowMultiple && totalBrowseItems > 1) { root.dispatch('DID_THROW_MAX_FILES', { source: items, error: createResponse('warning', 0, 'Max files'), }); return true; } // limit max items to one if not allowed to drop multiple items maxItems = allowMultiple ? maxItems : 1; if (!allowMultiple && allowReplace) { // There is only one item, so there is room to replace or add an item return false; } // no more room? const hasMaxItems = isInt(maxItems); if (hasMaxItems && totalItems + totalBrowseItems > maxItems) { root.dispatch('DID_THROW_MAX_FILES', { source: items, error: createResponse('warning', 0, 'Max files'), }); return true; } return false; }; const getDragIndex = (list, children, position) => { const itemList = list.childViews[0]; return getItemIndexByPosition(itemList, children, { left: position.scopeLeft - itemList.rect.element.left, top: position.scopeTop - (list.rect.outer.top + list.rect.element.marginTop + list.rect.element.scrollTop), }); }; /** * Enable or disable file drop functionality */ const toggleDrop = root => { const isAllowed = root.query('GET_ALLOW_DROP'); const isDisabled = root.query('GET_DISABLED'); const enabled = isAllowed && !isDisabled; if (enabled && !root.ref.hopper) { const hopper = createHopper( root.element, items => { // allow quick validation of dropped items const beforeDropFile = root.query('GET_BEFORE_DROP_FILE') || (() => true); // all items should be validated by all filters as valid const dropValidation = root.query('GET_DROP_VALIDATION'); return dropValidation ? items.every( item => applyFilters('ALLOW_HOPPER_ITEM', item, { query: root.query, }).every(result => result === true) && beforeDropFile(item) ) : true; }, { filterItems: items => { const ignoredFiles = root.query('GET_IGNORED_FILES'); return items.filter(item => { if (isFile(item)) { return !ignoredFiles.includes(item.name.toLowerCase()); } return true; }); }, catchesDropsOnPage: root.query('GET_DROP_ON_PAGE'), requiresDropOnElement: root.query('GET_DROP_ON_ELEMENT'), } ); hopper.onload = (items, position) => { // get item children elements and sort based on list sort const list = root.ref.list.childViews[0]; const visibleChildren = list.childViews.filter(child => child.rect.element.height); const children = root .query('GET_ACTIVE_ITEMS') .map(item => visibleChildren.find(child => child.id === item.id)) .filter(item => item); applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(queue => { // these files don't fit so stop here if (exceedsMaxFiles(root, queue)) return false; // go root.dispatch('ADD_ITEMS', { items: queue, index: getDragIndex(root.ref.list, children, position), interactionMethod: InteractionMethod.DROP, }); }); root.dispatch('DID_DROP', { position }); root.dispatch('DID_END_DRAG', { position }); }; hopper.ondragstart = position => { root.dispatch('DID_START_DRAG', { position }); }; hopper.ondrag = debounce(position => { root.dispatch('DID_DRAG', { position }); }); hopper.ondragend = position => { root.dispatch('DID_END_DRAG', { position }); }; root.ref.hopper = hopper; root.ref.drip = root.appendChildView(root.createChildView(drip)); } else if (!enabled && root.ref.hopper) { root.ref.hopper.destroy(); root.ref.hopper = null; root.removeChildView(root.ref.drip); } }; /** * Enable or disable browse functionality */ const toggleBrowse = (root, props) => { const isAllowed = root.query('GET_ALLOW_BROWSE'); const isDisabled = root.query('GET_DISABLED'); const enabled = isAllowed && !isDisabled; if (enabled && !root.ref.browser) { root.ref.browser = root.appendChildView( root.createChildView(browser, { ...props, onload: items => { applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch, }).then(queue => { // these files don't fit so stop here if (exceedsMaxFiles(root, queue)) return false; // add items! root.dispatch('ADD_ITEMS', { items: queue, index: -1, interactionMethod: InteractionMethod.BROWSE, }); }); }, }), 0 ); } else if (!enabled && root.ref.browser) { root.removeChildView(root.ref.browser); root.ref.browser = null; } }; /** * Enable or disable paste functionality */ const togglePaste = root => { const isAllowed = root.query('GET_ALLOW_PASTE'); const isDisabled = root.query('GET_DISABLED'); const enabled = isAllowed && !isDisabled; if (enabled && !root.ref.paster) { root.ref.paster = createPaster(); root.ref.paster.onload = items => { applyFilterChain('ADD_ITEMS', items, { dispatch: root.dispatch }).then(queue => { // these files don't fit so stop here if (exceedsMaxFiles(root, queue)) return false; // add items! root.dispatch('ADD_ITEMS', { items: queue, index: -1, interactionMethod: InteractionMethod.PASTE, }); }); }; } else if (!enabled && root.ref.paster) { root.ref.paster.destroy(); root.ref.paster = null; } }; /** * Route actions */ const route = createRoute({ DID_SET_ALLOW_BROWSE: ({ root, props }) => { toggleBrowse(root, props); }, DID_SET_ALLOW_DROP: ({ root }) => { toggleDrop(root); }, DID_SET_ALLOW_PASTE: ({ root }) => { togglePaste(root); }, DID_SET_DISABLED: ({ root, props }) => { toggleDrop(root); togglePaste(root); toggleBrowse(root, props); const isDisabled = root.query('GET_DISABLED'); if (isDisabled) { root.element.dataset.disabled = 'disabled'; } else { // delete root.element.dataset.disabled; <= this does not work on iOS 10 root.element.removeAttribute('data-disabled'); } }, }); export const root = createView({ name: 'root', read: ({ root }) => { if (root.ref.measure) { root.ref.measureHeight = root.ref.measure.offsetHeight; } }, create, write, destroy: ({ root }) => { if (root.ref.paster) { root.ref.paster.destroy(); } if (root.ref.hopper) { root.ref.hopper.destroy(); } root.element.removeEventListener('touchmove', prevent); root.element.removeEventListener('gesturestart', prevent); }, mixins: { styles: ['height'], }, }); ================================================ FILE: src/js/createApp.js ================================================ import { isNode } from './utils/isNode'; import { createAppObject } from './createAppObject'; import { createAppAtElement } from './createAppAtElement'; // if an element is passed, we create the instance at that element, if not, we just create an up object export const createApp = (...args) => isNode(args[0]) ? createAppAtElement(...args) : createAppObject(...args); ================================================ FILE: src/js/createAppAPI.js ================================================ import { copyObjectPropertiesToObject } from './utils/copyObjectPropertiesToObject'; const PRIVATE_METHODS = ['fire', '_read', '_write']; export const createAppAPI = app => { const api = {}; copyObjectPropertiesToObject(app, api, PRIVATE_METHODS); return api; }; ================================================ FILE: src/js/createAppAtElement.js ================================================ import { getAttributesAsObject } from './utils/getAttributesAsObject'; import { createAppObject } from './createAppObject'; import { applyFilters } from './filter'; import { isObject } from './utils/isObject'; export const createAppAtElement = (element, options = {}) => { // how attributes of the input element are mapped to the options for the plugin const attributeMapping = { // translate to other name '^class$': 'className', '^multiple$': 'allowMultiple', '^capture$': 'captureMethod', '^webkitdirectory$': 'allowDirectoriesOnly', // group under single property '^server': { group: 'server', mapping: { '^process': { group: 'process' }, '^revert': { group: 'revert' }, '^fetch': { group: 'fetch' }, '^restore': { group: 'restore' }, '^load': { group: 'load' } } }, // don't include in object '^type$': false, '^files$': false }; // add additional option translators applyFilters('SET_ATTRIBUTE_TO_OPTION_MAP', attributeMapping); // create final options object by setting options object and then overriding options supplied on element const mergedOptions = { ...options }; const attributeOptions = getAttributesAsObject( element.nodeName === 'FIELDSET' ? element.querySelector('input[type=file]') : element, attributeMapping ); // merge with options object Object.keys(attributeOptions).forEach(key => { if (isObject(attributeOptions[key])) { if (!isObject(mergedOptions[key])) { mergedOptions[key] = {}; } Object.assign(mergedOptions[key], attributeOptions[key]); } else { mergedOptions[key] = attributeOptions[key]; } }); // if parent is a fieldset, get files from parent by selecting all input fields that are not file upload fields // these will then be automatically set to the initial files mergedOptions.files = (options.files || []).concat( Array.from(element.querySelectorAll('input:not([type=file])')).map(input => ({ source: input.value, options: { type: input.dataset.type } })) ); // build plugin const app = createAppObject(mergedOptions); // add already selected files if (element.files) { Array.from(element.files).forEach(file => { app.addFile(file); }); } // replace the target element app.replaceElement(element); // expose return app; }; ================================================ FILE: src/js/createAppObject.js ================================================ import { createApp } from './app/index'; import { getOptions } from './app/options'; import { forin } from './utils/forin'; export const createAppObject = (customOptions = {}) => { // default options const defaultOptions = {}; forin(getOptions(), (key, value) => { defaultOptions[key] = value[0]; }); // set app options const app = createApp({ // default options ...defaultOptions, // custom options ...customOptions }); // return the plugin instance return app; }; ================================================ FILE: src/js/createAppPlugin.js ================================================ import { Type } from './app/enum/Type'; import { addFilter } from './filter'; import { applyFilterChain } from './filter'; import { forin } from './utils/forin'; import { replaceInString } from './utils/replaceInString'; import { toNaturalFileSize } from './utils/toNaturalFileSize'; import { isString } from './utils/isString'; import { guesstimateMimeType } from './utils/guesstimateMimeType'; import { getExtensionFromFilename } from './utils/getExtensionFromFilename'; import { getFileFromBlob } from './utils/getFileFromBlob'; import { getFilenameFromURL } from './utils/getFilenameFromURL'; import { getFilenameWithoutExtension } from './utils/getFilenameWithoutExtension'; import { createRoute } from './app/frame/createRoute'; import { createWorker } from './utils/createWorker'; import { createView } from './app/frame/createView'; import { loadImage } from './utils/loadImage'; import { copyFile } from './utils/copyFile'; import { renameFile } from './utils/renameFile'; import { extendDefaultOptions } from './app/options'; import { fileActionButton } from './app/view/fileActionButton'; import { isFile } from './utils/isFile'; import { createBlob } from './utils/createBlob'; import { text } from './utils/text'; import { getNumericAspectRatioFromString } from './utils/getNumericAspectRatioFromString'; import { createItemAPI } from './app/utils/createItemAPI'; // already registered plugins (can't register twice) const registeredPlugins = []; // pass utils to plugin export const createAppPlugin = plugin => { // already registered if (registeredPlugins.includes(plugin)) { return; } // remember this plugin registeredPlugins.push(plugin); // setup! const pluginOutline = plugin({ addFilter, utils: { Type, forin, isString, isFile, toNaturalFileSize, replaceInString, getExtensionFromFilename, getFilenameWithoutExtension, guesstimateMimeType, getFileFromBlob, getFilenameFromURL, createRoute, createWorker, createView, createItemAPI, loadImage, copyFile, renameFile, createBlob, applyFilterChain, text, getNumericAspectRatioFromString }, views: { fileActionButton } }); // add plugin options to default options extendDefaultOptions(pluginOutline.options); }; ================================================ FILE: src/js/filter.js ================================================ // all registered filters const filters = []; // loops over matching filters and passes options to each filter, returning the mapped results export const applyFilterChain = (key, value, utils) => new Promise((resolve, reject) => { // find matching filters for this key const matchingFilters = filters .filter(f => f.key === key) .map(f => f.cb); // resolve now if (matchingFilters.length === 0) { resolve(value); return; } // first filter to kick things of const initialFilter = matchingFilters.shift(); // chain filters matchingFilters .reduce( // loop over promises passing value to next promise (current, next) => current.then(value => next(value, utils)), // call initial filter, will return a promise initialFilter(value, utils) // all executed ) .then(value => resolve(value)) .catch(error => reject(error)); }); export const applyFilters = (key, value, utils) => filters.filter(f => f.key === key).map(f => f.cb(value, utils)); // adds a new filter to the list export const addFilter = (key, cb) => filters.push({ key, cb }); ================================================ FILE: src/js/index.js ================================================ import { createApp } from './createApp'; import { createPainter } from './app/frame/index'; import { createAppAPI } from './createAppAPI'; import { createAppPlugin } from './createAppPlugin'; import { getOptions as getDefaultOptions, setOptions as setDefaultOptions } from './app/options'; import { isObject } from './utils/isObject'; import { forin } from './utils/forin'; import { ItemStatus } from './app/enum/ItemStatus'; import { Status as StatusEnum } from './app/enum/Status'; import { FileOrigin as FileOriginEnum } from './app/enum/FileOrigin'; import { isBrowser } from './utils/isBrowser'; // feature detection used by supported() method const isOperaMini = () => Object.prototype.toString.call(window.operamini) === '[object OperaMini]'; const hasPromises = () => 'Promise' in window; const hasBlobSlice = () => 'slice' in Blob.prototype; const hasCreateObjectURL = () => 'URL' in window && 'createObjectURL' in window.URL; const hasVisibility = () => 'visibilityState' in document; const hasTiming = () => 'performance' in window; // iOS 8.x const hasCSSSupports = () => 'supports' in (window.CSS || {}); // use to detect Safari 9+ const isIE11 = () => /MSIE|Trident/.test(window.navigator.userAgent); export const supported = (() => { // Runs immediately and then remembers result for subsequent calls const isSupported = // Has to be a browser isBrowser() && // Can't run on Opera Mini due to lack of everything !isOperaMini() && // Require these APIs to feature detect a modern browser hasVisibility() && hasPromises() && hasBlobSlice() && hasCreateObjectURL() && hasTiming() && // doesn't need CSSSupports but is a good way to detect Safari 9+ (we do want to support IE11 though) (hasCSSSupports() || isIE11()); return () => isSupported; })(); /** * Plugin internal state (over all instances) */ const state = { // active app instances, used to redraw the apps and to find the later apps: [] }; // plugin name const name = 'filepond'; /** * Public Plugin methods */ const fn = () => {}; export let Status = {}; export let FileStatus = {}; export let FileOrigin = {}; export let OptionTypes = {}; export let create = fn; export let destroy = fn; export let parse = fn; export let find = fn; export let registerPlugin = fn; export let getOptions = fn; export let setOptions = fn; // if not supported, no API if (supported()) { // start painter and fire load event createPainter( () => { state.apps.forEach(app => app._read()); }, (ts) => { state.apps.forEach(app => app._write(ts)); } ); // fire loaded event so we know when FilePond is available const dispatch = () => { // let others know we have area ready document.dispatchEvent( new CustomEvent('FilePond:loaded', { detail: { supported, create, destroy, parse, find, registerPlugin, setOptions } }) ); // clean up event document.removeEventListener('DOMContentLoaded', dispatch); }; if (document.readyState !== 'loading') { // move to back of execution queue, FilePond should have been exported by then setTimeout(() => dispatch(), 0); } else { document.addEventListener('DOMContentLoaded', dispatch); } // updates the OptionTypes object based on the current options const updateOptionTypes = () => forin(getDefaultOptions(), (key, value) => { OptionTypes[key] = value[1]; }); Status = { ...StatusEnum }; FileOrigin = { ...FileOriginEnum }; FileStatus = { ...ItemStatus }; OptionTypes = {}; updateOptionTypes(); // create method, creates apps and adds them to the app array create = (...args) => { const app = createApp(...args); app.on('destroy', destroy); state.apps.push(app); return createAppAPI(app); }; // destroys apps and removes them from the app array destroy = hook => { // returns true if the app was destroyed successfully const indexToRemove = state.apps.findIndex(app => app.isAttachedTo(hook)); if (indexToRemove >= 0) { // remove from apps const app = state.apps.splice(indexToRemove, 1)[0]; // restore original dom element app.restoreElement(); return true; } return false; }; // parses the given context for plugins (does not include the context element itself) parse = context => { // get all possible hooks const matchedHooks = Array.from(context.querySelectorAll(`.${name}`)); // filter out already active hooks const newHooks = matchedHooks.filter( newHook => !state.apps.find(app => app.isAttachedTo(newHook)) ); // create new instance for each hook return newHooks.map(hook => create(hook)); }; // returns an app based on the given element hook find = hook => { const app = state.apps.find(app => app.isAttachedTo(hook)); if (!app) { return null; } return createAppAPI(app); }; // adds a plugin extension registerPlugin = (...plugins) => { // register plugins plugins.forEach(createAppPlugin); // update OptionTypes, each plugin might have extended the default options updateOptionTypes(); } getOptions = () => { const opts = {}; forin(getDefaultOptions(), (key, value) => { opts[key] = value[0]; }); return opts; } setOptions = opts => { if (isObject(opts)) { // update existing plugins state.apps.forEach(app => { app.setOptions(opts); }); // override defaults setDefaultOptions(opts); } // return new options return getOptions(); }; } ================================================ FILE: src/js/utils/arrayInsert.js ================================================ export const arrayInsert = (arr, index, item) => arr.splice(index, 0, item); ================================================ FILE: src/js/utils/arrayRemove.js ================================================ export const arrayRemove = (arr, index) => arr.splice(index, 1); ================================================ FILE: src/js/utils/arrayReverse.js ================================================ export const arrayReverse = arr => arr.reverse(); ================================================ FILE: src/js/utils/attr.js ================================================ export const attr = (node, name, value = null) => { if (value === null) { return node.getAttribute(name) || node.hasAttribute(name); } node.setAttribute(name, value); }; ================================================ FILE: src/js/utils/attrToggle.js ================================================ import { attr } from './attr'; export const attrToggle = (element, name, state, enabledValue = '') => { if (state) { attr(element, name, enabledValue); } else { element.removeAttribute(name); } }; ================================================ FILE: src/js/utils/canUpdateFileInput.js ================================================ let res = null; export const canUpdateFileInput = () => { if (res === null) { try { const dataTransfer = new DataTransfer(); dataTransfer.items.add(new File(['hello world'], 'This_Works.txt')); const el = document.createElement('input'); el.setAttribute('type', 'file'); el.files = dataTransfer.files; res = el.files.length === 1; } catch (err) { res = false; } } return res; }; ================================================ FILE: src/js/utils/capitalizeFirstLetter.js ================================================ export const capitalizeFirstLetter = string => string.charAt(0).toUpperCase() + string.slice(1); ================================================ FILE: src/js/utils/composeObject.js ================================================ export const composeObject = (...objects) => { return Object.assign({}, ...objects); }; ================================================ FILE: src/js/utils/copyFile.js ================================================ import { renameFile } from './renameFile'; export const copyFile = file => renameFile(file, file.name); ================================================ FILE: src/js/utils/copyObjectPropertiesToObject.js ================================================ export const copyObjectPropertiesToObject = (src, target, excluded) => { Object.getOwnPropertyNames(src) .filter(property => !excluded.includes(property)) .forEach(key => Object.defineProperty( target, key, Object.getOwnPropertyDescriptor(src, key) ) ); }; ================================================ FILE: src/js/utils/createBlob.js ================================================ import { getBlobBuilder } from './getBlobBuilder'; export const createBlob = (arrayBuffer, mimeType) => { const BB = getBlobBuilder(); if (BB) { const bb = new BB(); bb.append(arrayBuffer); return bb.getBlob(mimeType); } return new Blob([arrayBuffer], { type: mimeType }); }; ================================================ FILE: src/js/utils/createDefaultResponse.js ================================================ import { createResponse } from './createResponse'; export const createTimeoutResponse = (cb) => (xhr) => { cb( createResponse( 'error', 0, 'Timeout', xhr.getAllResponseHeaders() ) ) } ================================================ FILE: src/js/utils/createElement.js ================================================ export const createElement = tagName => { return document.createElement(tagName); }; ================================================ FILE: src/js/utils/createObject.js ================================================ import { defineProperty } from './defineProperty'; import { forin } from './forin'; export const createObject = definition => { const obj = {}; forin(definition, property => { defineProperty(obj, property, definition[property]); }); return obj; }; ================================================ FILE: src/js/utils/createResponse.js ================================================ export const createResponse = (type, code, body, headers) => ({ type, code, body, headers }); ================================================ FILE: src/js/utils/createWorker.js ================================================ import { getUniqueId } from './getUniqueId'; export const createWorker = fn => { const workerBlob = new Blob(['(', fn.toString(), ')()'], { type: 'application/javascript' }); const workerURL = URL.createObjectURL(workerBlob); const worker = new Worker(workerURL); return { transfer: (message, cb) => {}, post: (message, cb, transferList) => { const id = getUniqueId(); worker.onmessage = e => { if (e.data.id === id) { cb(e.data.message); } }; worker.postMessage( { id, message }, transferList ); }, terminate: () => { worker.terminate(); URL.revokeObjectURL(workerURL); } }; }; ================================================ FILE: src/js/utils/debounce.js ================================================ export const debounce = (func, interval = 16, immidiateOnly = true) => { let last = Date.now(); let timeout = null; return (...args) => { clearTimeout(timeout); const dist = Date.now() - last; const fn = () => { last = Date.now(); func(...args); }; if (dist < interval) { // we need to delay by the difference between interval and dist // for example: if distance is 10 ms and interval is 16 ms, // we need to wait an additional 6ms before calling the function) if (!immidiateOnly) { timeout = setTimeout(fn, interval - dist); } } else { // go! fn(); } }; }; ================================================ FILE: src/js/utils/deepCloneObject.js ================================================ import { isObject } from './isObject'; import { isArray } from './isArray'; export const deepCloneObject = (src) => { if (!isObject(src)) return src; const target = isArray(src) ? [] : {}; for (const key in src) { if (!src.hasOwnProperty(key)) continue; const v = src[key]; target[key] = v && isObject(v) ? deepCloneObject(v) : v; } return target; } ================================================ FILE: src/js/utils/defineProperty.js ================================================ export const defineProperty = (obj, property, definition) => { if (typeof definition === 'function') { obj[property] = definition; return; } Object.defineProperty(obj, property, { ...definition }); }; ================================================ FILE: src/js/utils/describeArc.js ================================================ import { polarToCartesian } from './polarToCartesian'; export const describeArc = (x, y, radius, startAngle, endAngle, arcSweep) => { const start = polarToCartesian(x, y, radius, endAngle); const end = polarToCartesian(x, y, radius, startAngle); return [ 'M', start.x, start.y, 'A', radius, radius, 0, arcSweep, 0, end.x, end.y ].join(' '); }; ================================================ FILE: src/js/utils/forEachDelayed.js ================================================ export const forEachDelayed = (items, cb, delay = 75) => items.map( (item, index) => new Promise((resolve, reject) => { setTimeout(() => { cb(item); resolve(); }, delay * index); }) ); ================================================ FILE: src/js/utils/forin.js ================================================ export const forin = (obj, cb) => { for (const key in obj) { if (!obj.hasOwnProperty(key)) { continue; } cb(key, obj[key]); } }; ================================================ FILE: src/js/utils/formatFilename.js ================================================ export const formatFilename = name => name; ================================================ FILE: src/js/utils/fromCamels.js ================================================ export const fromCamels = (string, separator = '-') => string .split(/(?=[A-Z])/) .map(part => part.toLowerCase()) .join(separator); ================================================ FILE: src/js/utils/getAttributesAsObject.js ================================================ import { toCamels } from './toCamels'; import { attr } from './attr'; import { forin } from './forin'; import { lowerCaseFirstLetter } from './lowerCaseFirstLetter'; import { isString } from './isString'; import { isObject } from './isObject'; const attributeNameToPropertyName = attributeName => toCamels(attributeName.replace(/^data-/, '')); const mapObject = (object, propertyMap) => { // remove unwanted forin(propertyMap, (selector, mapping) => { forin(object, (property, value) => { // create regexp shortcut const selectorRegExp = new RegExp(selector); // tests if const matches = selectorRegExp.test(property); // no match, skip if (!matches) { return; } // if there's a mapping, the original property is always removed delete object[property]; // should only remove, we done! if (mapping === false) { return; } // move value to new property if (isString(mapping)) { object[mapping] = value return; } // move to group const group = mapping.group; if (isObject(mapping) && !object[group]) { object[group] = {}; } object[group][ lowerCaseFirstLetter(property.replace(selectorRegExp, '')) ] = value; }); // do submapping if (mapping.mapping) { mapObject(object[mapping.group], mapping.mapping); } }); }; export const getAttributesAsObject = (node, attributeMapping = {}) => { // turn attributes into object const attributes = []; forin(node.attributes, index => { attributes.push(node.attributes[index]); }); const output = attributes .filter(attribute => attribute.name) .reduce((obj, attribute) => { const value = attr( node, attribute.name ); obj[attributeNameToPropertyName(attribute.name)] = value === attribute.name ? true : value; return obj; }, {}); // do mapping of object properties mapObject(output, attributeMapping); return output; }; ================================================ FILE: src/js/utils/getBase64DataFromBase64DataURI.js ================================================ export const getBase64DataFromBase64DataURI = dataURI => { // get data part of string (remove data:image/jpeg...,) const data = dataURI.split(',')[1]; // remove any whitespace as that causes InvalidCharacterError in IE return data.replace(/\s/g, ''); }; ================================================ FILE: src/js/utils/getBlobBuilder.js ================================================ export const getBlobBuilder = () => { return (window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder); }; ================================================ FILE: src/js/utils/getBlobFromBase64DataURI.js ================================================ import { getBlobFromByteStringWithMimeType } from './getBlobFromByteStringWithMimeType'; import { getMimeTypeFromBase64DataURI } from './getMimeTypeFromBase64DataURI'; import { getByteStringFromBase64DataURI } from './getByteStringFromBase64DataURI'; export const getBlobFromBase64DataURI = dataURI => { const mimeType = getMimeTypeFromBase64DataURI(dataURI); const byteString = getByteStringFromBase64DataURI(dataURI); return getBlobFromByteStringWithMimeType(byteString, mimeType); }; ================================================ FILE: src/js/utils/getBlobFromByteStringWithMimeType.js ================================================ import { createBlob } from './createBlob'; export const getBlobFromByteStringWithMimeType = (byteString, mimeType) => { const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(ab); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } return createBlob(ab, mimeType); }; ================================================ FILE: src/js/utils/getByteStringFromBase64DataURI.js ================================================ import { getBase64DataFromBase64DataURI } from './getBase64DataFromBase64DataURI'; export const getByteStringFromBase64DataURI = dataURI => { return atob(getBase64DataFromBase64DataURI(dataURI)); }; ================================================ FILE: src/js/utils/getDateString.js ================================================ import { leftPad } from './leftPad'; export const getDateString = (date = new Date()) => `${date.getFullYear()}-${leftPad(date.getMonth() + 1, '00')}-${leftPad( date.getDate(), '00' )}_${leftPad(date.getHours(), '00')}-${leftPad( date.getMinutes(), '00' )}-${leftPad(date.getSeconds(), '00')}`; ================================================ FILE: src/js/utils/getDecimalSeparator.js ================================================ import { getNonNumeric } from './getNonNumeric'; export const getDecimalSeparator = () => getNonNumeric((1.1).toLocaleString())[0]; ================================================ FILE: src/js/utils/getDomainFromURL.js ================================================ export const getDomainFromURL = url => { if (url.indexOf('//') === 0) { url = location.protocol + url; } return url .toLowerCase() .replace('blob:', '') .replace(/([a-z])?:\/\//, '$1') .split('/')[0]; }; ================================================ FILE: src/js/utils/getExtensionFromFilename.js ================================================ export const getExtensionFromFilename = name => name.split('.').pop(); ================================================ FILE: src/js/utils/getFileFromBase64DataURI.js ================================================ import { getFileFromBlob } from './getFileFromBlob'; import { getBlobFromBase64DataURI } from './getBlobFromBase64DataURI'; export const getFileFromBase64DataURI = (dataURI, filename, extension) => { return getFileFromBlob( getBlobFromBase64DataURI(dataURI), filename, null, extension ); }; ================================================ FILE: src/js/utils/getFileFromBlob.js ================================================ import { getExtensionFromFilename } from './getExtensionFromFilename'; import { guesstimateExtension } from './guesstimateExtension'; import { isString } from './isString'; import { getDateString } from './getDateString'; export const getFileFromBlob = ( blob, filename, type = null, extension = null ) => { const file = typeof type === 'string' ? blob.slice(0, blob.size, type) : blob.slice(0, blob.size, blob.type); file.lastModifiedDate = new Date(); // copy relative path if (blob._relativePath) file._relativePath = blob._relativePath; // if blob has name property, use as filename if no filename supplied if (!isString(filename)) { filename = getDateString(); } // if filename supplied but no extension and filename has extension if (filename && extension === null && getExtensionFromFilename(filename)) { file.name = filename; } else { extension = extension || guesstimateExtension(file.type); file.name = filename + (extension ? '.' + extension : ''); } return file; }; ================================================ FILE: src/js/utils/getFileInfoFromHeaders.js ================================================ export const getFileNameFromHeader = header => { // test if is content disposition header, if not exit if (!/^content-disposition:/i.test(header)) return null; // get filename parts const matches = header.split(/filename=|filename\*=.+''/) .splice(1) .map(name => name.trim().replace(/^["']|[;"']{0,2}$/g, '')) .filter(name => name.length) return matches.length ? decodeURI(matches[matches.length-1]) : null; } const getFileSizeFromHeader = header => { if (/content-length:/i.test(header)) { const size = header.match(/[0-9]+/)[0]; return size ? parseInt(size, 10) : null; } return null; } const getTranfserIdFromHeader = header => { if (/x-content-transfer-id:/i.test(header)) { const id = (header.split(':')[1] || '').trim(); return id || null; } return null; } export const getFileInfoFromHeaders = headers => { const info = { source: null, name: null, size: null }; const rows = headers.split('\n'); for (let header of rows) { const name = getFileNameFromHeader(header); if (name) { info.name = name; continue; } const size = getFileSizeFromHeader(header); if (size) { info.size = size; continue; } const source = getTranfserIdFromHeader(header); if (source) { info.source = source; continue; } } return info; } ================================================ FILE: src/js/utils/getFilenameFromURL.js ================================================ export const getFilenameFromURL = url => `${url}` .split('/') .pop() .split('?') .shift(); ================================================ FILE: src/js/utils/getFilenameWithoutExtension.js ================================================ export const getFilenameWithoutExtension = name => name.substring(0, name.lastIndexOf('.')) || name; ================================================ FILE: src/js/utils/getMimeTypeFromBase64DataURI.js ================================================ export const getMimeTypeFromBase64DataURI = dataURI => { return (/^data:(.+);/.exec(dataURI) || [])[1] || null; }; ================================================ FILE: src/js/utils/getNonNumeric.js ================================================ export const getNonNumeric = str => /[^0-9]+/.exec(str); ================================================ FILE: src/js/utils/getNumericAspectRatioFromString.js ================================================ import { isEmpty } from './isEmpty'; export const getNumericAspectRatioFromString = aspectRatio => { if (isEmpty(aspectRatio)) { return aspectRatio; } if(/:/.test(aspectRatio)) { const parts = aspectRatio.split(':'); return parts[1] / parts[0]; } return parseFloat(aspectRatio); }; ================================================ FILE: src/js/utils/getParameters.js ================================================ export const getParameters = (args, filters) => { return Object.keys(filters).reduce((acc, name) => { acc[name] = args.find(arg => typeof arg === filters[name]); return acc; }, {}); }; ================================================ FILE: src/js/utils/getRandomNumber.js ================================================ export const getRandomNumber = (min = 0, max = 1) => min + Math.random() * (max - min); ================================================ FILE: src/js/utils/getRootNode.js ================================================ export const getRootNode = element => 'getRootNode' in element ? element.getRootNode() : document; ================================================ FILE: src/js/utils/getThousandsSeparator.js ================================================ import { getNonNumeric } from './getNonNumeric'; import { getDecimalSeparator } from './getDecimalSeparator'; export const getThousandsSeparator = () => { // Added for browsers that do not return the thousands separator (happend on native browser Android 4.4.4) // We check against the normal toString output and if they're the same return a comma when decimal separator is a dot const decimalSeparator = getDecimalSeparator(); const thousandsStringWithSeparator = (1000.0).toLocaleString(); const thousandsStringWithoutSeparator = (1000.0).toString(); if (thousandsStringWithSeparator !== thousandsStringWithoutSeparator) { return getNonNumeric(thousandsStringWithSeparator)[0]; } return decimalSeparator === '.' ? ',' : '.'; }; ================================================ FILE: src/js/utils/getUniqueId.js ================================================ export const getUniqueId = () => Math.random() .toString(36) .substring(2, 11); ================================================ FILE: src/js/utils/guesstimateExtension.js ================================================ export const guesstimateExtension = type => { // if no extension supplied, exit here if (typeof type !== 'string') { return ''; } // get subtype const subtype = type.split('/').pop(); // is svg subtype if (/svg/.test(subtype)) { return 'svg'; } if (/zip|compressed/.test(subtype)) { return 'zip'; } if (/plain/.test(subtype)) { return 'txt'; } if (/msword/.test(subtype)) { return 'doc'; } // if is valid subtype if (/[a-z]+/.test(subtype)) { // always use jpg extension if (subtype === 'jpeg') { return 'jpg'; } // return subtype return subtype; } return ''; }; ================================================ FILE: src/js/utils/guesstimateMimeType.js ================================================ const images = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'tiff']; const text = ['css', 'csv', 'html', 'txt']; const map = { zip: 'zip|compressed', epub: 'application/epub+zip' }; export const guesstimateMimeType = (extension = '') => { extension = extension.toLowerCase(); if (images.includes(extension)) { return ( 'image/' + (extension === 'jpg' ? 'jpeg' : extension === 'svg' ? 'svg+xml' : extension) ); } if (text.includes(extension)) { return 'text/' + extension; } return map[extension] || ''; }; ================================================ FILE: src/js/utils/hasQueryString.js ================================================ export const hasQueryString = url => /[?&]/.test(url); ================================================ FILE: src/js/utils/insertAfter.js ================================================ export const insertAfter = (newNode, referenceNode) => { return referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling ); }; ================================================ FILE: src/js/utils/insertBefore.js ================================================ export const insertBefore = (newNode, referenceNode) => referenceNode.parentNode.insertBefore(newNode, referenceNode); ================================================ FILE: src/js/utils/isArray.js ================================================ export const isArray = value => Array.isArray(value); ================================================ FILE: src/js/utils/isBase64DataURI.js ================================================ export const isBase64DataURI = str => /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*)\s*$/i.test( str ); ================================================ FILE: src/js/utils/isBoolean.js ================================================ export const isBoolean = value => typeof value === 'boolean'; ================================================ FILE: src/js/utils/isBrowser.js ================================================ const IS_BROWSER = (() => typeof window !== 'undefined' && typeof window.document !== 'undefined')(); export const isBrowser = () => IS_BROWSER; ================================================ FILE: src/js/utils/isDefined.js ================================================ export const isDefined = value => value != null; ================================================ FILE: src/js/utils/isEmpty.js ================================================ export const isEmpty = value => value == null; ================================================ FILE: src/js/utils/isExternalURL.js ================================================ import { getDomainFromURL } from './getDomainFromURL'; export const isExternalURL = url => (url.indexOf(':') > -1 || url.indexOf('//') > -1) && getDomainFromURL(location.href) !== getDomainFromURL(url); ================================================ FILE: src/js/utils/isFile.js ================================================ export const isFile = value => !!(value instanceof File || (value instanceof Blob && value.name)); ================================================ FILE: src/js/utils/isFunction.js ================================================ export const isFunction = value => typeof value === 'function'; ================================================ FILE: src/js/utils/isIOS.js ================================================ let testResult = null; export const isIOS = () => { if (testResult === null) { testResult = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; } return testResult; } ================================================ FILE: src/js/utils/isInt.js ================================================ import { isNumber } from './isNumber'; export const isInt = value => isNumber(value) && isFinite(value) && Math.floor(value) === value; ================================================ FILE: src/js/utils/isNode.js ================================================ export const isNode = value => value instanceof HTMLElement; ================================================ FILE: src/js/utils/isNull.js ================================================ export const isNull = value => value === null; ================================================ FILE: src/js/utils/isNumber.js ================================================ export const isNumber = value => typeof value === 'number'; ================================================ FILE: src/js/utils/isObject.js ================================================ export const isObject = value => typeof value === 'object' && value !== null; ================================================ FILE: src/js/utils/isString.js ================================================ export const isString = value => typeof value === 'string'; ================================================ FILE: src/js/utils/leftPad.js ================================================ export const leftPad = (value, padding = '') => (padding + value).slice(-padding.length); ================================================ FILE: src/js/utils/limit.js ================================================ export const limit = (value, min, max) => Math.max(Math.min(max, value), min); ================================================ FILE: src/js/utils/loadImage.js ================================================ export const loadImage = (url) => new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { resolve(img); }; img.onerror = e => { reject(e); }; img.src = url; }); ================================================ FILE: src/js/utils/lowerCaseFirstLetter.js ================================================ export const lowerCaseFirstLetter = string => string.charAt(0).toLowerCase() + string.slice(1); ================================================ FILE: src/js/utils/percentageArc.js ================================================ import { describeArc } from './describeArc'; export const percentageArc = (x, y, radius, from, to) => { let arcSweep = 1; if (to > from && to - from <= 0.5) { arcSweep = 0; } if (from > to && from - to >= 0.5) { arcSweep = 0; } return describeArc( x, y, radius, Math.min(0.9999, from) * 360, Math.min(0.9999, to) * 360, arcSweep ); }; ================================================ FILE: src/js/utils/polarToCartesian.js ================================================ export const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => { const angleInRadians = (angleInDegrees % 360 - 90) * Math.PI / 180.0; return { x: centerX + radius * Math.cos(angleInRadians), y: centerY + radius * Math.sin(angleInRadians) }; }; ================================================ FILE: src/js/utils/renameFile.js ================================================ export const renameFile = (file, name) => { const renamedFile = file.slice(0, file.size, file.type); renamedFile.lastModifiedDate = file.lastModifiedDate; renamedFile.name = name; return renamedFile; }; ================================================ FILE: src/js/utils/replaceInString.js ================================================ /** * Replaces placeholders in given string with replacements * @param string - "Foo {bar}"" * @param replacements - { "bar": 10 } */ export const replaceInString = (string, replacements) => string.replace(/(?:{([a-zA-Z]+)})/g, (match, group) => replacements[group]); ================================================ FILE: src/js/utils/resetFileInput.js ================================================ import { createElement } from './createElement'; export const resetFileInput = input => { // no value, no need to reset if (!input || input.value === '') { return; } try { // for modern browsers input.value = ''; } catch (err) {} // for IE10 if (input.value) { // quickly append input to temp form and reset form const form = createElement('form'); const parentNode = input.parentNode; const ref = input.nextSibling; form.appendChild(input); form.reset(); // re-inject input where it originally was if (ref) { parentNode.insertBefore(input, ref); } else { parentNode.appendChild(input); } } }; ================================================ FILE: src/js/utils/sendRequest.js ================================================ import { isInt } from './isInt'; const isGet = method => /GET|HEAD/.test(method); export const sendRequest = (data, url, options) => { const api = { onheaders: () => {}, onprogress: () => {}, onload: () => {}, ontimeout: () => {}, onerror: () => {}, onabort: () => {}, abort: () => { aborted = true; xhr.abort(); } }; // timeout identifier, only used when timeout is defined let aborted = false; let headersReceived = false; // set default options options = { method: 'POST', headers: {}, withCredentials: false, ...options }; // encode url url = encodeURI(url); // if method is GET, add any received data to url if (isGet(options.method) && data) { url = `${url}${encodeURIComponent( typeof data === 'string' ? data : JSON.stringify(data) )}`; } // create request const xhr = new XMLHttpRequest(); // progress of load const process = isGet(options.method) ? xhr : xhr.upload; process.onprogress = e => { // no progress event when aborted ( onprogress is called once after abort() ) if (aborted) { return; } api.onprogress(e.lengthComputable, e.loaded, e.total); }; // tries to get header info to the app as fast as possible xhr.onreadystatechange = () => { // not interesting in these states ('unsent' and 'openend' as they don't give us any additional info) if (xhr.readyState < 2) { return; } // no server response if (xhr.readyState === 4 && xhr.status === 0) { return; } if (headersReceived) { return; } headersReceived = true; // we've probably received some useful data in response headers api.onheaders(xhr); }; // load successful xhr.onload = () => { // is classified as valid response if (xhr.status >= 200 && xhr.status < 300) { api.onload(xhr); } else { api.onerror(xhr); } }; // error during load xhr.onerror = () => api.onerror(xhr); // request aborted xhr.onabort = () => { aborted = true; api.onabort(); }; // request timeout xhr.ontimeout = () => api.ontimeout(xhr); // open up open up! xhr.open(options.method, url, true); // set timeout if defined (do it after open so IE11 plays ball) if (isInt(options.timeout)) { xhr.timeout = options.timeout; } // add headers Object.keys(options.headers).forEach(key => { const value = unescape(encodeURIComponent(options.headers[key])); xhr.setRequestHeader(key, value); }); // set type of response if (options.responseType) { xhr.responseType = options.responseType; } // set credentials if (options.withCredentials) { xhr.withCredentials = true; } // let's send our data xhr.send(data); return api; }; ================================================ FILE: src/js/utils/setInputFiles.js ================================================ export const setInputFiles = (element, files) => { try { // Create a DataTransfer instance and add a newly created file const dataTransfer = new DataTransfer(); files.forEach(file => { if (file instanceof File) { dataTransfer.items.add(file); } else { dataTransfer.items.add( new File([file], file.name, { type: file.type, }) ); } }); // Assign the DataTransfer files list to the file input element.files = dataTransfer.files; } catch (err) { return false; } return true; }; ================================================ FILE: src/js/utils/text.js ================================================ export const text = (node, value) => { let textNode = node.childNodes[0]; if (!textNode) { textNode = document.createTextNode(value); node.appendChild(textNode); } else if (value !== textNode.nodeValue) { textNode.nodeValue = value; } }; ================================================ FILE: src/js/utils/toArray.js ================================================ import { isArray } from './isArray'; import { isEmpty } from './isEmpty'; import { trim } from './trim'; import { toString } from './toString'; export const toArray = (value, splitter = ',') => { if (isEmpty(value)) { return []; } if (isArray(value)) { return value; } return toString(value) .split(splitter) .map(trim) .filter(str => str.length); }; ================================================ FILE: src/js/utils/toBoolean.js ================================================ import { isBoolean } from './isBoolean'; export const toBoolean = value => (isBoolean(value) ? value : value === 'true'); ================================================ FILE: src/js/utils/toBytes.js ================================================ import { toString } from './toString'; import { isInt } from './isInt'; import { toInt } from './toInt'; export const toBytes = (value, base = 1000) => { // is in bytes if (isInt(value)) { return value; } // is natural file size let naturalFileSize = toString(value).trim(); // if is value in megabytes if (/MB$/i.test(naturalFileSize)) { naturalFileSize = naturalFileSize.replace(/MB$i/, '').trim(); return toInt(naturalFileSize) * base * base; } // if is value in kilobytes if (/KB/i.test(naturalFileSize)) { naturalFileSize = naturalFileSize.replace(/KB$i/, '').trim(); return toInt(naturalFileSize) * base; } return toInt(naturalFileSize); }; ================================================ FILE: src/js/utils/toCamels.js ================================================ export const toCamels = (string, separator = '-') => string.replace(new RegExp(`${separator}.`, 'g'), sub => sub.charAt(1).toUpperCase() ); ================================================ FILE: src/js/utils/toFloat.js ================================================ import { toNumber } from './toNumber'; export const toFloat = value => parseFloat(toNumber(value)); ================================================ FILE: src/js/utils/toFunctionReference.js ================================================ export const toFunctionReference = string => { let ref = self; let levels = string.split('.'); let level = null; while ((level = levels.shift())) { ref = ref[level]; if (!ref) { return null; } } return ref; }; ================================================ FILE: src/js/utils/toInt.js ================================================ import { toNumber } from './toNumber'; export const toInt = value => parseInt(toNumber(value), 10); ================================================ FILE: src/js/utils/toNaturalFileSize.js ================================================ export const toNaturalFileSize = (bytes, decimalSeparator = '.', base = 1000, options = {}) => { const { labelBytes = 'bytes', labelKilobytes = 'KB', labelMegabytes = 'MB', labelGigabytes = 'GB', } = options; // no negative byte sizes bytes = Math.round(Math.abs(bytes)); const KB = base; const MB = base * base; const GB = base * base * base; // just bytes if (bytes < KB) { return `${bytes} ${labelBytes}`; } // kilobytes if (bytes < MB) { return `${Math.floor(bytes / KB)} ${labelKilobytes}`; } // megabytes if (bytes < GB) { return `${removeDecimalsWhenZero(bytes / MB, 1, decimalSeparator)} ${labelMegabytes}`; } // gigabytes return `${removeDecimalsWhenZero(bytes / GB, 2, decimalSeparator)} ${labelGigabytes}`; }; const removeDecimalsWhenZero = (value, decimalCount, separator) => { return value .toFixed(decimalCount) .split('.') .filter(part => part !== '0') .join(separator); }; ================================================ FILE: src/js/utils/toNumber.js ================================================ import { isNumber } from './isNumber'; import { isString } from './isString'; import { toString } from './toString'; export const toNumber = value => isNumber(value) ? value : isString(value) ? toString(value).replace(/[a-z]+/gi, '') : 0; ================================================ FILE: src/js/utils/toPercentage.js ================================================ export const toPercentage = value => Math.round(value * 100); ================================================ FILE: src/js/utils/toString.js ================================================ export const toString = value => '' + value; ================================================ FILE: src/js/utils/trim.js ================================================ export const trim = str => str.trim(); ================================================ FILE: types/index.d.ts ================================================ // Based on definitions by Zach Posten for React-Filepond // Updated by FilePond Contributors /* Disable no-redundant-jsdoc since @default statements are NOT redundant */ /* tslint:disable:no-redundant-jsdoc */ export {}; export enum FileStatus { INIT = 1, IDLE = 2, PROCESSING_QUEUED = 9, PROCESSING = 3, PROCESSING_COMPLETE = 5, PROCESSING_ERROR = 6, PROCESSING_REVERT_ERROR = 10, LOADING = 7, LOAD_ERROR = 8, } export enum Status { EMPTY = 0, IDLE = 1, ERROR = 2, BUSY = 3, READY = 4, } export enum FileOrigin { INPUT = 1, LIMBO = 2, LOCAL = 3, } // TODO replace all references to `ActualFileObject` with native `File` /** * @deprecated Don't use this type explicitly within your code. It'll be replaced with the native `File` type in a future release. */ export type ActualFileObject = Blob & { readonly lastModified: number; readonly name: string }; /** * A custom FilePond File. */ export class FilePondFile { /** Returns the ID of the file. */ id: string; /** Returns the server id of the file. */ serverId: string; /** Returns the source of the file. */ source: ActualFileObject | string; /** Returns the origin of the file. */ origin: FileOrigin; /** Returns the current status of the file. */ status: FileStatus; /** Returns the File object. */ file: ActualFileObject; /** Returns the file extensions. */ fileExtension: string; /** Returns the size of the file. */ fileSize: number; /** Returns the type of the file. */ fileType: string; /** Returns the full name of the file. */ filename: string; /** Returns the name of the file without extension. */ filenameWithoutExtension: string; /** Aborts loading of this file */ abortLoad: () => void; /** Aborts processing of this file */ abortProcessing: () => void; /** * Retrieve metadata saved to the file, pass a key to retrieve * a specific part of the metadata (e.g. 'crop' or 'resize'). * If no key is passed, the entire metadata object is returned. */ getMetadata: (key?: string) => any; /** Add additional metadata to the file */ setMetadata: (key: string, value: any, silent?: boolean) => void; } // TODO delete /** * A custom FilePond File. Don't confuse this with the native `File` type. * * @deprecated use `FilePondFile` instead. This type will be removed in a future release. */ export class File extends FilePondFile {} export interface ServerUrl { url: string; method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; withCredentials?: boolean; headers?: { [key: string]: string | boolean | number }; timeout?: number; /** * Called when server response is received, useful for getting * the unique file id from the server response. */ onload?: (response: any) => number | string; /** * Called when server error is received, receives the response * body, useful to select the relevant error data. */ onerror?: (responseBody: any) => any; /** * Called with the formdata object right before it is sent, * return extended formdata object to make changes. */ ondata?: (data: FormData) => FormData; } export type ProgressServerConfigFunction = ( /** * Flag indicating if the resource has a length that can be calculated. * If not, the totalDataAmount has no significant value. Setting this to * false switches the FilePond loading indicator to infinite mode. */ isLengthComputable: boolean, /** The amount of data currently transferred. */ loadedDataAmount: number, /** The total amount of data to be transferred. */ totalDataAmount: number ) => void; export interface ProcessServerChunkTransferOptions { chunkTransferId: string; chunkServer: ServerUrl; /** * Chunk uploads enabled */ chunkUploads: boolean; /** * Forcing use of chunk uploads even for files smaller than chunk size */ chunkForce: boolean; /** * Size of chunks */ chunkSize: number; /** * Amount of times to retry upload of a chunk when it fails */ chunkRetryDelays: number[]; } export type ProcessServerConfigFunction = ( /** The name of the input field. */ fieldName: string, /** The actual file object to send. */ file: ActualFileObject, metadata: { [key: string]: any }, /** * Should call the load method when done and pass the returned server file id. * This server file id is then used later on when reverting or restoring a file * so that your server knows which file to return without exposing that info * to the client. */ load: (p: string | { [key: string]: any }) => void, /** Call if something goes wrong, will exit after. */ error: (errorText: string) => void, /** * Should call the progress method to update the progress to 100% before calling load(). * Setting computable to false switches the loading indicator to infinite mode. */ progress: ProgressServerConfigFunction, /** Let FilePond know the request has been cancelled. */ abort: () => void, /** * Let Filepond know and store the current file chunk transfer id so it can track the * progress of the whole file upload */ transfer: (transferId: string) => void, options: ProcessServerChunkTransferOptions ) => void; export type RevertServerConfigFunction = ( /** Server file id of the file to restore. */ uniqueFieldId: any, /** Should call the load method when done. */ load: () => void, /** Call if something goes wrong, will exit after. */ error: (errorText: string) => void ) => void; export type RestoreServerConfigFunction = ( /** Server file id of the file to restore. */ uniqueFileId: any, /** Should call the load method with a file object or blob when done. */ load: (file: ActualFileObject) => void, /** Call if something goes wrong, will exit after. */ error: (errorText: string) => void, /** * Should call the progress method to update the progress to 100% before calling load(). * Setting computable to false switches the loading indicator to infinite mode. */ progress: ProgressServerConfigFunction, /** Let FilePond know the request has been cancelled. */ abort: () => void, /** * Can call the headers method to supply FilePond with early response header string. * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getAllResponseHeaders */ headers: (headersString: string) => void ) => void; export type LoadServerConfigFunction = ( source: any, /** Should call the load method with a file object or blob when done. */ load: (file: ActualFileObject | Blob) => void, /** Call if something goes wrong, will exit after. */ error: (errorText: string) => void, /** * Should call the progress method to update the progress to 100% before calling load(). * Setting computable to false switches the loading indicator to infinite mode. */ progress: ProgressServerConfigFunction, /** Let FilePond know the request has been cancelled. */ abort: () => void, /** * Can call the headers method to supply FilePond with early response header string. * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getAllResponseHeaders> */ headers: (headersString: string) => void ) => void; export type FetchServerConfigFunction = ( url: string, /** Should call the load method with a file object or blob when done. */ load: (file: ActualFileObject | Blob) => void, /** Call if something goes wrong, will exit after. */ error: (errorText: string) => void, /** * Should call the progress method to update the progress to 100% before calling load(). * Setting computable to false switches the loading indicator to infinite mode. */ progress: ProgressServerConfigFunction, /** Let FilePond know the request has been cancelled. */ abort: () => void, /** * Can call the headers method to supply FilePond with early response header string. * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getAllResponseHeaders */ headers: (headersString: string) => void ) => void; export type RemoveServerConfigFunction = ( /** Local file source */ source: any, /** Call when done */ load: () => void, /** Call if something goes wrong, will exit after. */ error: (errorText: string) => void ) => void; export interface FilePondInitialFile { /** The server file reference. */ source: string; options: { /** Origin of file being added. */ type: 'input' | 'limbo' | 'local'; /** Mock file information. */ file?: { name?: string; size?: number; type?: string; }; /** File initial metadata. */ metadata?: { [key: string]: any }; }; } export interface FilePondServerConfigProps { /** * Immediately upload new files to the server. * @default true */ instantUpload?: boolean; /** * The maximum number of files that can be uploaded in parallel. * @default 2 */ maxParallelUploads?: number; /** * Server API Configuration. * See: https://pqina.nl/filepond/docs/patterns/api/server * @default null */ server?: | string | { url?: string; timeout?: number; headers?: { [key: string]: string | boolean | number }; process?: string | ServerUrl | ProcessServerConfigFunction | null; revert?: string | ServerUrl | RevertServerConfigFunction | null; restore?: string | ServerUrl | RestoreServerConfigFunction | null; load?: string | ServerUrl | LoadServerConfigFunction | null; fetch?: string | ServerUrl | FetchServerConfigFunction | null; patch?: string | ServerUrl | null; remove?: RemoveServerConfigFunction | null; } | null; /** * Enable chunk uploads * @default false */ chunkUploads?: boolean; /** * Force use of chunk uploads even for files smaller than chunk size * @default false */ chunkForce?: boolean; /** * Size of chunks (5MB default) * @default 5000000 */ chunkSize?: number; /** * Amount of times to retry upload of a chunk when it fails * @default [500, 1000, 3000] */ chunkRetryDelays?: number[]; /** * A list of file locations that should be loaded immediately. * See: https://pqina.nl/filepond/docs/patterns/api/filepond-object/#setting-initial-files * @default [] */ files?: Array; } export interface FilePondDragDropProps { /** * FilePond will catch all files dropped on the webpage. * @default false */ dropOnPage?: boolean; /** * Require drop on the FilePond element itself to catch the file. * @default true */ dropOnElement?: boolean; /** * When enabled, files are validated before they are dropped. * A file is not added when it’s invalid. * @default false */ dropValidation?: boolean; /** * Ignored file names when handling dropped directories. * Dropping directories is not supported on all browsers. * @default ['.ds_store', 'thumbs.db', 'desktop.ini'] */ ignoredFiles?: string[]; } export interface FilePondLabelProps { /** * The decimal separator used to render numbers. * By default this is determined automatically. * @default 'auto' */ labelDecimalSeparator?: string; /** * The thousands separator used to render numbers. * By default this is determined automatically. * @default 'auto' */ labelThousandsSeparator?: string; /** * Default label shown to indicate this is a drop area. * FilePond will automatically bind browse file events to * the element with CSS class .filepond--label-action. * @default 'Drag & Drop your files or Browse ' */ labelIdle?: string; /** * Label shown when the field contains invalid files and is validated by the parent form. * @default 'Field contains invalid files' */ labelInvalidField?: string; /** * Label used while waiting for file size information. * @default 'Waiting for size' */ labelFileWaitingForSize?: string; /** * Label used when no file size information was received. * @default 'Size not available' */ labelFileSizeNotAvailable?: string; /** * Label used to indicate bytes * @default 'Bytes' */ labelFileSizeBytes?: string; /** * Label used to indicate kilobytes * @default 'KB' */ labelFileSizeKilobytes?: string; /** * Label used to indicate megabytes * @default 'MB' */ labelFileSizeMegabytes?: string; /** * Label used to indicate gigabytes * @default 'GB' */ labelFileSizeGigabytes?: string; /** * Label used when showing the number of files and there is only one. * @default 'file in list' */ labelFileCountSingular?: string; /** * Label used when showing the number of files and there is more than one. * @default 'files in list' */ labelFileCountPlural?: string; /** * Label used while loading a file. * @default 'Loading' */ labelFileLoading?: string; /** * Label used when file is added (assistive only). * @default 'Added' */ labelFileAdded?: string; /** * Label used when file load failed. * @default 'Error during load' */ labelFileLoadError?: ((error: any) => string) | string; /** * Label used when file is removed (assistive only). * @default 'Removed' */ labelFileRemoved?: string; /** * Label used when something went during during removing the file upload. * @default 'Error during remove' */ labelFileRemoveError?: ((error: any) => string) | string; /** * Label used when uploading a file. * @default 'Uploading' */ labelFileProcessing?: string; /** * Label used when file upload has completed. * @default 'Upload complete' */ labelFileProcessingComplete?: string; /** * Label used when upload was cancelled. * @default 'Upload cancelled' */ labelFileProcessingAborted?: string; /** * Label used when something went wrong during file upload. * @default 'Error during upload' */ labelFileProcessingError?: ((error: any) => string) | string; /** * Label used when something went wrong during reverting the file upload. * @default 'Error during revert' */ labelFileProcessingRevertError?: ((error: any) => string) | string; /** * Label used to indicate to the user that an action can be cancelled. * @default 'tap to cancel' */ labelTapToCancel?: string; /** * Label used to indicate to the user that an action can be retried. * @default 'tap to retry' */ labelTapToRetry?: string; /** * Label used to indicate to the user that an action can be undone. * @default 'tap to undo' */ labelTapToUndo?: string; /** * Label used for remove button. * @default 'Remove' */ labelButtonRemoveItem?: string; /** * Label used for abort load button. * @default 'Abort' */ labelButtonAbortItemLoad?: string; /** * Label used for retry load. * @default 'Retry' */ labelButtonRetryItemLoad?: string; /** * Label used for abort upload button. * @default 'Cancel' */ labelButtonAbortItemProcessing?: string; /** * Label used for undo upload button. * @default 'Undo' */ labelButtonUndoItemProcessing?: string; /** * Label used for retry upload button. * @default 'Retry' */ labelButtonRetryItemProcessing?: string; /** * Label used for upload button. * @default 'Upload' */ labelButtonProcessItem?: string; } export interface FilePondSvgIconProps { /** * The icon used for remove actions. * @default '' */ iconRemove?: string; /** * The icon used for process actions. * @default '' */ iconProcess?: string; /** * The icon used for retry actions. * @default '' */ iconRetry?: string; /** * The icon used for undo actions. * @default '' */ iconUndo?: string; /** * The icon used for done. * @default '' */ iconDone?: string; } export interface FilePondErrorDescription { type: string; code: number; body: string; } export interface FilePondCallbackProps { /** FilePond instance has been created and is ready. */ oninit?: () => void; /** * FilePond instance throws a warning. For instance * when the maximum amount of files has been reached. * Optionally receives file if error is related to a * file object. */ onwarning?: (error: any, file?: FilePondFile, status?: any) => void; /** * FilePond instance throws an error. Optionally receives * file if error is related to a file object. */ onerror?: (error: FilePondErrorDescription, file?: FilePondFile, status?: any) => void; /** Started file load. */ onaddfilestart?: (file: FilePondFile) => void; /** Made progress loading a file. */ onaddfileprogress?: (file: FilePondFile, progress: number) => void; /** If no error, file has been successfully loaded. */ onaddfile?: (error: FilePondErrorDescription | null, file: FilePondFile) => void; /** Started processing a file. */ onprocessfilestart?: (file: FilePondFile) => void; /** Made progress processing a file. */ onprocessfileprogress?: (file: FilePondFile, progress: number) => void; /** Aborted processing of a file. */ onprocessfileabort?: (file: FilePondFile) => void; /** Processing of a file has been reverted. */ onprocessfilerevert?: (file: FilePondFile) => void; /** If no error, Processing of a file has been completed. */ onprocessfile?: (error: FilePondErrorDescription | null, file: FilePondFile) => void; /** Called when all files in the list have been processed. */ onprocessfiles?: () => void; /** File has been removed. */ onremovefile?: (error: FilePondErrorDescription | null, file: FilePondFile) => void; /** * File has been transformed by the transform plugin or * another plugin subscribing to the prepare_output filter. * It receives the file item and the output data. */ onpreparefile?: (file: FilePondFile, output: any) => void; /** A file has been added or removed, receives a list of file items. */ onupdatefiles?: (files: FilePondFile[]) => void; /* Called when a file is clicked or tapped. **/ onactivatefile?: (file: FilePondFile) => void; /** Called when the files have been reordered */ onreorderfiles?: (files: FilePondFile[]) => void; } export interface FilePondHookProps { /** * FilePond is about to allow this item to be dropped, it can be a URL or a File object. * * Return `true` or `false` depending on if you want to allow the item to be dropped. */ beforeDropFile?: (file: FilePondFile | string) => boolean; /** * FilePond is about to add this file. * * Return `false` to prevent adding it, or return a `Promise` and resolve with `true` or `false`. */ beforeAddFile?: (item: FilePondFile) => boolean | Promise; /** * FilePond is about to remove this file. * * Return `false` to prevent adding it, or return a `Promise` and resolve with `true` or `false`. */ beforeRemoveFile?: (item: FilePondFile) => boolean | Promise; } export interface FilePondStyleProps { /** * Set a different layout render mode. * @default null */ stylePanelLayout?: | 'integrated' | 'compact' | 'circle' | 'integrated circle' | 'compact circle' | null; /** * Set a forced aspect ratio for the FilePond drop area. * * Accepts human readable aspect ratios like `1:1` or numeric aspect ratios like `0.75`. * @default null */ stylePanelAspectRatio?: string | null; /** * Set a forced aspect ratio for the file items. * * Useful when rendering cropped or fixed aspect ratio images in grid view. * @default null */ styleItemPanelAspectRatio?: string | null; /** * The position of the remove item button. * @default 'left' */ styleButtonRemoveItemPosition?: string; /** * The position of the remove item button. * @default 'right' */ styleButtonProcessItemPosition?: string; /** * The position of the load indicator. * @default 'right' */ styleLoadIndicatorPosition?: string; /** * The position of the progress indicator. * @default 'right' */ styleProgressIndicatorPosition?: string; /** * Enable to align the remove button to the left side of the file item. * @default false */ styleButtonRemoveItemAlign?: boolean; } export type CaptureAttribute = 'camera' | 'microphone' | 'camcorder'; export interface FilePondBaseProps { /** * The ID to add to the root element. * @default null */ id?: string | null; /** * The input field name to use. * @default 'filepond' */ name?: string; /** * Class Name to put on wrapper. * @default null */ className?: string | null; /** * Sets the required attribute to the output field. * @default false */ required?: boolean; /** * Sets the disabled attribute to the output field. * @default false */ disabled?: boolean; /** * Sets the given value to the capture attribute. * @default null */ captureMethod?: CaptureAttribute | null; /** * Set to false to prevent FilePond from setting the file input field `accept` attribute to the value of the `acceptedFileTypes`. */ allowSyncAcceptAttribute?: boolean; /** * Enable or disable drag n’ drop. * @default true */ allowDrop?: boolean; /** * Enable or disable file browser. * @default true */ allowBrowse?: boolean; /** * Enable or disable pasting of files. Pasting files is not * supported on all browsers. * @default true */ allowPaste?: boolean; /** * Enable or disable adding multiple files. * @default false */ allowMultiple?: boolean; /** * Allow drop to replace a file, only works when allowMultiple is false. * @default true */ allowReplace?: boolean; /** * Allows the user to revert file upload. * @default true */ allowRevert?: boolean; /** * When set to false the remove button is hidden and disabled. * @default true */ allowRemove?: boolean; /** * Allows user to process a file. When set to false, this removes the file upload button. * @default true */ allowProcess?: boolean; /** * Allows the user to reorder the file items * @default false */ allowReorder?: boolean; /** * Allow only selecting directories with browse (no support for filtering dnd at this point) * @default false */ allowDirectoriesOnly?: boolean; /** * Require the file to be successfully reverted before continuing. * @default false */ forceRevert?: boolean; /** * The maximum number of files that filepond pond can handle. * @default null */ maxFiles?: number | null; /** * Enables custom validity messages. * @default false */ checkValidity?: boolean; /** * Set to false to always add items to beginning or end of list. * @default true */ itemInsertLocationFreedom?: boolean; /** * Default index in list to add items that have been dropped at the top of the list. * @default 'before' */ itemInsertLocation?: 'before' | 'after' | ((a: FilePondFile, b: FilePondFile) => number); /** * The interval to use before showing each item being added to the list. * @default 75 */ itemInsertInterval?: number; /** * The base value used to calculate file size * @default 1000 */ fileSizeBase?: number; /** * Tells FilePond to store files in hidden file input elements so they can be posted along with * normal form post. This only works if the browser supports the DataTransfer constructor, * this is the case on Firefox, Chrome, Chromium powered browsers and Safari version 14.1 * and higher. * @default false */ storeAsFile?: boolean; /** * Shows Powered by PQINA in footer. Can be disabled by setting to false, but please do * link to https://pqina.nl somewhere else on your website, or otherwise donate to help * keep the project alive. * @default "Powered by PQINA" */ credits?: false; } // TODO delete /** * @deprecated use `FilePondOptions`. This will be removed in a future release. */ export interface FilePondOptionProps extends FilePondDragDropProps, FilePondServerConfigProps, FilePondLabelProps, FilePondSvgIconProps, FilePondCallbackProps, FilePondHookProps, FilePondStyleProps, FilePondBaseProps {} export interface FilePondOptions extends FilePondDragDropProps, FilePondServerConfigProps, FilePondLabelProps, FilePondSvgIconProps, FilePondCallbackProps, FilePondHookProps, FilePondStyleProps, FilePondBaseProps {} export type FilePondEventPrefixed = | 'FilePond:init' | 'FilePond:warning' | 'FilePond:error' | 'FilePond:addfilestart' | 'FilePond:addfileprogress' | 'FilePond:addfile' | 'FilePond:processfilestart' | 'FilePond:processfileprogress' | 'FilePond:processfileabort' | 'FilePond:processfilerevert' | 'FilePond:processfile' | 'FilePond:processfiles' | 'FilePond:removefile' | 'FilePond:updatefiles' | 'FilePond:reorderfiles'; export type FilePondEvent = | 'init' | 'warning' | 'error' | 'addfilestart' | 'addfileprogress' | 'addfile' | 'processfilestart' | 'processfileprogress' | 'processfileabort' | 'processfilerevert' | 'processfile' | 'processfiles' | 'removefile' | 'updatefiles' | 'reorderfiles'; export interface RemoveFileOptions { remove?: boolean; revert?: boolean; } export interface FilePond extends Required {} export class FilePond { /** * The root element of the Filepond instance. */ readonly element: Element | null; /** * Returns the current status of the FilePond instance. * @default Status.EMPTY */ readonly status: Status; /** Override multiple options at once. */ setOptions(options: FilePondOptions): void; /** * Adds a file. * @param options.index The index that the file should be added at. */ addFile( source: ActualFileObject | Blob | string, options?: { index?: number } & Partial ): Promise; /** * Adds multiple files. * @param options.index The index that the files should be added at. */ addFiles( source: ActualFileObject[] | Blob[] | string[], options?: { index: number } ): Promise; /** * Moves a file. Select file with query and supply target index. * @param query The file reference, id, or index. * @param index The index to move the file to. */ moveFile(query: FilePondFile | string | number, index: number): void; /** * Removes a file. * @param query The file reference, id, or index. If no query is provided, removes the first file in the list. * @param options Options for removal */ removeFile(query?: FilePondFile | string | number, options?: RemoveFileOptions): void; /** * Removes the first file in the list. * @param options Options for removal */ removeFile(options: RemoveFileOptions): void; /** * Removes files matching the query. * @param query Array containing file references, ids, and/or indexes. If no array is provided, all files are removed * @param options Options for removal */ removeFiles(query?: Array, options?: RemoveFileOptions): void; /** * Removes all files. * @param options Options for removal */ removeFiles(options: RemoveFileOptions): void; /** * Processes a file. If no parameter is provided, processes the first file in the list. * @param query The file reference, id, or index */ processFile(query?: FilePondFile | string | number): Promise; /** * Processes multiple files. If no parameter is provided, processes all files. * @param query The file reference(s), id(s), or index(es) */ processFiles(query?: FilePondFile[] | string[] | number[]): Promise; /** * Starts preparing the file matching the given query, returns a Promise, the Promise is resolved with the file item and the output file { file, output } * @param query The file reference, id, or index */ prepareFile( query?: FilePondFile | string | number ): Promise<{ file: FilePondFile; output: any }>; /** * Processes multiple files. If no parameter is provided, processes all files. * @param query Array containing file reference(s), id(s), or index(es) */ prepareFiles( query?: FilePondFile[] | string[] | number[] ): Promise>; /** * Returns a file. If no parameter is provided, returns the first file in the list. * @param query The file id, or index */ getFile(query?: string | number): FilePondFile; /** Returns all files. */ getFiles(): FilePondFile[]; /** * Manually trigger the browse files panel. * * Only works if the call originates from the user. */ browse(): void; /** * Sort the items in the files list. * @param compare The comparison function */ sort(compare: (a: FilePondFile, b: FilePondFile) => number): void; /** Destroys this FilePond instance. */ destroy(): void; /** Inserts the FilePond instance after the supplied element. */ insertAfter(element: Element): void; /** Inserts the FilePond instance before the supplied element. */ insertBefore(element: Element): void; /** Appends FilePond to the given element. */ appendTo(element: Element): void; /** Returns true if the current instance is attached to the supplied element. */ isAttachedTo(element: Element): void; /** Replaces the supplied element with FilePond. */ replaceElement(element: Element): void; /** If FilePond replaced the original element, this restores the original element to its original glory. */ restoreElement(element: Element): void; /** * Adds an event listener to the given event. * @param event Name of the event, prefixed with `Filepond:` * @param fn Event handler */ addEventListener(event: FilePondEventPrefixed, fn: (e: any) => void): void; /** * Listen to an event. * @param event Name of the event * @param fn Event handler, signature is identical to the callback method */ on(event: FilePondEvent, fn: (...args: any[]) => void): void; /** * Listen to an event once and remove the handler. * @param event Name of the event * @param fn Event handler, signature is identical to the callback method */ onOnce(event: FilePondEvent, fn: (...args: any[]) => void): void; /** * Stop listening to an event. * @param event Name of the event * @param fn Event handler, signature is identical to the callback method */ off(event: FilePondEvent, fn: (...args: any[]) => void): void; } /** Creates a new FilePond instance. */ export function create(element?: Element, options?: FilePondOptions): FilePond; /** Destroys the FilePond instance attached to the supplied element. */ export function destroy(element: Element): void; /** Returns the FilePond instance attached to the supplied element. */ export function find(element: Element): FilePond; /** * Parses a given section of the DOM tree for elements with class * .filepond and turns them into FilePond elements. */ export function parse(context: Element): void; /** Registers a FilePond plugin for later use. */ export function registerPlugin(...plugins: any[]): void; /** Sets page level default options for all FilePond instances. */ export function setOptions(options: FilePondOptions): void; /** Returns the current default options. */ export function getOptions(): FilePondOptions; /** Determines whether or not the browser supports FilePond. */ export function supported(): boolean; /** Returns an object describing all the available options and their types, useful for writing FilePond adapters. */ export const OptionTypes: object; ================================================ FILE: types/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "lib": ["es6", "dom"], "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, "strictFunctionTypes": true, "noEmit": true, // If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index". // If the library is global (cannot be imported via `import` or `require`), leave this out. "baseUrl": ".", "paths": { "filepond": ["."] } } }