FilePondPluginImageEditor.esm.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. /*!
  2. * Pintura Image Editor 8.13.1
  3. * (c) 2018-2021 PQINA Inc. - All Rights Reserved
  4. * License: https://pqina.nl/pintura/license/
  5. */
  6. /* eslint-disable */
  7. var isFile = (v) => v instanceof File;
  8. var isImage = (file) => /^image/.test(file.type);
  9. var isString = (v) => typeof v === 'string';
  10. let isSafari = null;
  11. var isSafari$1 = () => {
  12. if (isSafari === null)
  13. isSafari = isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
  14. return isSafari;
  15. };
  16. var getImageElementSize = (imageElement) => new Promise((resolve, reject) => {
  17. let shouldAutoRemove = false;
  18. // test if image is attached to DOM, if not attached, attach so measurement is correct on Safari
  19. if (!imageElement.parentNode && isSafari$1()) {
  20. shouldAutoRemove = true;
  21. // has width 0 and height 0 to prevent rendering very big SVGs (without width and height) that will for one frame overflow the window and show a scrollbar
  22. imageElement.style.cssText = `position:absolute;visibility:hidden;pointer-events:none;left:0;top:0;width:0;height:0;`;
  23. document.body.appendChild(imageElement);
  24. }
  25. // start testing size
  26. const measure = () => {
  27. const width = imageElement.naturalWidth;
  28. const height = imageElement.naturalHeight;
  29. const hasSize = width && height;
  30. if (!hasSize)
  31. return;
  32. // clean up image if was attached for measuring
  33. if (shouldAutoRemove)
  34. imageElement.parentNode.removeChild(imageElement);
  35. clearInterval(intervalId);
  36. resolve({ width, height });
  37. };
  38. imageElement.onerror = (err) => {
  39. clearInterval(intervalId);
  40. reject(err);
  41. };
  42. const intervalId = setInterval(measure, 1);
  43. measure();
  44. });
  45. var getImageSize = async (image) => {
  46. // the image element we'll use to load the image
  47. let imageElement = image;
  48. // if is not an image element, it must be a valid image source
  49. if (!imageElement.src) {
  50. imageElement = new Image();
  51. imageElement.src = isString(image) ? image : URL.createObjectURL(image);
  52. }
  53. let size;
  54. try {
  55. size = await getImageElementSize(imageElement);
  56. }
  57. finally {
  58. isFile(image) && URL.revokeObjectURL(imageElement.src);
  59. }
  60. return size;
  61. };
  62. var isBlob = (v) => v instanceof Blob && !(v instanceof File);
  63. var noop = (...args) => { };
  64. let result$3 = null;
  65. var isBrowser = () => {
  66. if (result$3 === null)
  67. result$3 = typeof window !== 'undefined' && typeof window.document !== 'undefined';
  68. return result$3;
  69. };
  70. var h = (name, attributes, children = []) => {
  71. const el = document.createElement(name);
  72. // @ts-ignore
  73. const descriptors = Object.getOwnPropertyDescriptors(el.__proto__);
  74. for (const key in attributes) {
  75. if (key === 'style') {
  76. el.style.cssText = attributes[key];
  77. }
  78. else if ((descriptors[key] && descriptors[key].set) ||
  79. /textContent|innerHTML/.test(key) ||
  80. typeof attributes[key] === 'function') {
  81. el[key] = attributes[key];
  82. }
  83. else {
  84. el.setAttribute(key, attributes[key]);
  85. }
  86. }
  87. children.forEach((child) => el.appendChild(child));
  88. return el;
  89. };
  90. var releaseCanvas = (canvas) => {
  91. canvas.width = 1;
  92. canvas.height = 1;
  93. const ctx = canvas.getContext('2d');
  94. ctx && ctx.clearRect(0, 0, 1, 1);
  95. };
  96. let result$2 = null;
  97. var supportsWebGL2 = () => {
  98. if (result$2 === null) {
  99. if ('WebGL2RenderingContext' in window) {
  100. let canvas;
  101. try {
  102. canvas = h('canvas');
  103. result$2 = !!canvas.getContext('webgl2');
  104. }
  105. catch (err) {
  106. result$2 = false;
  107. }
  108. canvas && releaseCanvas(canvas);
  109. }
  110. else {
  111. result$2 = false;
  112. }
  113. }
  114. return result$2;
  115. };
  116. var getWebGLContext = (canvas, attrs) => {
  117. if (supportsWebGL2())
  118. return canvas.getContext('webgl2', attrs);
  119. return (canvas.getContext('webgl', attrs) ||
  120. canvas.getContext('experimental-webgl', attrs));
  121. };
  122. let result$1 = null;
  123. var supportsWebGL = () => {
  124. if (result$1 === null) {
  125. const canvas = h('canvas');
  126. result$1 = !!getWebGLContext(canvas);
  127. releaseCanvas(canvas);
  128. }
  129. return result$1;
  130. };
  131. const isOperaMini = () => Object.prototype.toString.call(window['operamini']) === '[object OperaMini]';
  132. const hasPromises = () => 'Promise' in window;
  133. const hasCreateObjectURL = () => 'URL' in window && 'createObjectURL' in window.URL;
  134. const hasVisibility = () => 'visibilityState' in document;
  135. const hasTiming = () => 'performance' in window; // iOS 8.x
  136. const hasFileConstructor = () => 'File' in window; // excludes IE11
  137. let result = null;
  138. var isModernBrowser = () => {
  139. if (result === null)
  140. result =
  141. isBrowser() &&
  142. // Can't run on Opera Mini due to lack of everything
  143. !isOperaMini() &&
  144. // Require these APIs to feature detect a modern browser
  145. hasVisibility() &&
  146. hasPromises() &&
  147. hasFileConstructor() &&
  148. hasCreateObjectURL() &&
  149. hasTiming();
  150. return result;
  151. };
  152. /**
  153. * Image Edit Proxy Plugin
  154. */
  155. const plugin = (_) => {
  156. const { addFilter, utils, views } = _;
  157. const { Type, createRoute } = utils;
  158. const { fileActionButton } = views;
  159. const getEditorSafe = (editor) => (editor === null ? {} : editor);
  160. addFilter('SHOULD_REMOVE_ON_REVERT', (shouldRemove, { item, query }) => new Promise((resolve) => {
  161. const { file } = item;
  162. // if this file is editable it shouldn't be removed immidiately even when instant uploading
  163. const canEdit = query('GET_ALLOW_IMAGE_EDITOR') &&
  164. query('GET_IMAGE_EDITOR_ALLOW_EDIT') &&
  165. query('GET_IMAGE_EDITOR_SUPPORT_EDIT') &&
  166. query('GET_IMAGE_EDITOR_SUPPORT_IMAGE')(file);
  167. // if the file cannot be edited it should be removed on revert
  168. resolve(!canEdit);
  169. }));
  170. // open editor when loading a new item
  171. addFilter('DID_LOAD_ITEM', (item, { query, dispatch }) => new Promise((resolve, reject) => {
  172. // if is temp or local file
  173. if (item.origin > 1) {
  174. resolve(item);
  175. return;
  176. }
  177. // get file reference
  178. const { file } = item;
  179. if (!query('GET_ALLOW_IMAGE_EDITOR') ||
  180. !query('GET_IMAGE_EDITOR_INSTANT_EDIT' ))
  181. return resolve(item);
  182. // exit if this is not an image
  183. if (!query('GET_IMAGE_EDITOR_SUPPORT_IMAGE')(file))
  184. return resolve(item);
  185. // request editing of a file
  186. const requestEdit = () => {
  187. if (!editRequestQueue.length)
  188. return;
  189. const { item, resolve, reject } = editRequestQueue[0];
  190. dispatch('EDIT_ITEM', {
  191. id: item.id,
  192. handleEditorResponse: createEditorResponseHandler(item, resolve, reject),
  193. });
  194. };
  195. // is called when the user confirms editing the file
  196. const createEditorResponseHandler = (item, resolve, reject) => (userDidConfirm) => {
  197. // remove item
  198. editRequestQueue.shift();
  199. // handle item
  200. if (userDidConfirm) {
  201. resolve(item);
  202. }
  203. else {
  204. reject(item);
  205. }
  206. // TODO: Fix, should not be needed to kick the internal loop in case no processes are running
  207. dispatch('KICK');
  208. // handle next item!
  209. requestEdit();
  210. };
  211. queueEditRequest({ item, resolve, reject });
  212. if (editRequestQueue.length === 1) {
  213. requestEdit();
  214. }
  215. }));
  216. // extend item methods
  217. addFilter('DID_CREATE_ITEM', (item, { query, dispatch }) => {
  218. item.extend('edit', () => {
  219. dispatch('EDIT_ITEM', { id: item.id });
  220. });
  221. });
  222. const editRequestQueue = [];
  223. const queueEditRequest = (editRequest) => {
  224. editRequestQueue.push(editRequest);
  225. return editRequest;
  226. };
  227. const couldTransformFile = (query) => {
  228. // no editor defined, then exit
  229. const { imageProcessor, imageReader, imageWriter } = getEditorSafe(query('GET_IMAGE_EDITOR'));
  230. return (query('GET_IMAGE_EDITOR_WRITE_IMAGE') &&
  231. query('GET_IMAGE_EDITOR_SUPPORT_WRITE_IMAGE') &&
  232. imageProcessor &&
  233. imageReader &&
  234. imageWriter);
  235. };
  236. // called for each view that is created right after the 'create' method
  237. addFilter('CREATE_VIEW', (viewAPI) => {
  238. // get reference to created view
  239. const { is, view, query } = viewAPI;
  240. if (!query('GET_ALLOW_IMAGE_EDITOR'))
  241. return;
  242. if (!query('GET_IMAGE_EDITOR_SUPPORT_WRITE_IMAGE'))
  243. return;
  244. const supportsFilePoster = query('GET_ALLOW_FILE_POSTER');
  245. // only run for either the file or the file info panel
  246. const shouldExtendView = (is('file-info') && !supportsFilePoster) || (is('file') && supportsFilePoster);
  247. if (!shouldExtendView)
  248. return;
  249. // no editor defined, then exit
  250. const { createEditor, imageProcessor, imageReader, imageWriter, editorOptions, legacyDataToImageState, imageState: imageBaseState, } = getEditorSafe(query('GET_IMAGE_EDITOR'));
  251. if (!imageReader || !imageWriter || !editorOptions || !editorOptions.locale)
  252. return;
  253. // remove default image reader and writer if set
  254. delete editorOptions.imageReader;
  255. delete editorOptions.imageWriter;
  256. const [createImageReader, imageReaderOptions] = imageReader;
  257. const [createImageWriter = noop, imageWriterOptions] = imageWriter;
  258. // tests if file item has poster
  259. const getItemByProps = (props) => {
  260. const { id } = props;
  261. const item = query('GET_ITEM', id);
  262. return item;
  263. };
  264. const hasPoster = (root, props) => {
  265. if (!query('GET_ALLOW_FILE_POSTER'))
  266. return false;
  267. const item = getItemByProps(props);
  268. if (!item)
  269. return;
  270. // test if is filtered
  271. if (!query('GET_FILE_POSTER_FILTER_ITEM')(item))
  272. return false;
  273. const poster = item.getMetadata('poster');
  274. return !!poster;
  275. };
  276. // generate preview
  277. const getPosterTargetSize = (root, targetSize) => {
  278. const posterHeight = root.query('GET_FILE_POSTER_HEIGHT');
  279. const maxPosterHeight = root.query('GET_FILE_POSTER_MAX_HEIGHT');
  280. if (posterHeight) {
  281. targetSize.width = posterHeight * 2;
  282. targetSize.height = posterHeight * 2;
  283. }
  284. else if (maxPosterHeight) {
  285. targetSize.width = maxPosterHeight * 2;
  286. targetSize.height = maxPosterHeight * 2;
  287. }
  288. return targetSize;
  289. };
  290. const createEditorOptions = (root) => {
  291. const targetSize = getPosterTargetSize(root, {
  292. width: 512,
  293. height: 512,
  294. });
  295. return {
  296. ...editorOptions,
  297. imageReader: createImageReader(imageReaderOptions),
  298. imageWriter: createImageWriter({
  299. // poster size
  300. targetSize,
  301. // can optionally overwrite poster size
  302. ...(imageWriterOptions || {}),
  303. }),
  304. };
  305. };
  306. const createImagePoster = ({ root, props }) => {
  307. // need image processor to create image poster
  308. if (!imageProcessor)
  309. return;
  310. const item = getItemByProps(props);
  311. if (!item)
  312. return;
  313. const file = item.file;
  314. const imageState = item.getMetadata('imageState');
  315. const options = {
  316. ...createEditorOptions(root),
  317. imageState: {
  318. ...imageBaseState,
  319. ...imageState,
  320. },
  321. };
  322. imageProcessor(file, options).then(({ dest }) => {
  323. item.setMetadata('poster', URL.createObjectURL(dest), true);
  324. });
  325. };
  326. // opens the editor, if it does not already exist, it creates the editor
  327. const openImageEditor = ({ root, props, action }) => {
  328. const { handleEditorResponse } = action;
  329. // get item
  330. const item = getItemByProps(props);
  331. // file to open
  332. const file = item.file;
  333. // open the editor (sets editor properties and imageState property)
  334. const editor = createEditor({
  335. ...createEditorOptions(root),
  336. src: file,
  337. });
  338. // when the image has loaded, update the editor
  339. editor.on('load', ({ size }) => {
  340. // get current image edit state
  341. let imageState = item.getMetadata('imageState');
  342. imageState = legacyDataToImageState
  343. ? legacyDataToImageState(editor, size, imageState)
  344. : imageState;
  345. // update editor view based on image edit state
  346. editor.imageState = {
  347. ...imageBaseState,
  348. ...imageState,
  349. };
  350. });
  351. editor.on('process', ({ imageState, dest }) => {
  352. // if already has post URL, try to revoke
  353. const poster = item.getMetadata('poster');
  354. poster && URL.revokeObjectURL(item.getMetadata('poster'));
  355. // store state, two seperate actions because we want to trigger preparefile when setting `imageState`
  356. item.setMetadata('poster', URL.createObjectURL(dest));
  357. item.setMetadata('imageState', imageState);
  358. // used in instant edit mode
  359. if (!handleEditorResponse)
  360. return;
  361. handleEditorResponse(true);
  362. });
  363. editor.on('close', () => {
  364. // used in instant edit mode
  365. if (!handleEditorResponse)
  366. return;
  367. handleEditorResponse(false);
  368. });
  369. };
  370. //#region view
  371. const didLoadItem = ({ root, props }) => {
  372. const { id } = props;
  373. // try to access item
  374. const item = query('GET_ITEM', id);
  375. if (!item)
  376. return;
  377. // get the file object
  378. const file = item.file;
  379. // exit if this is not an image
  380. if (!query('GET_IMAGE_EDITOR_SUPPORT_IMAGE')(file))
  381. return;
  382. if (query('GET_ALLOW_FILE_POSTER') && !item.getMetadata('poster')) {
  383. root.dispatch('REQUEST_CREATE_IMAGE_POSTER', { id });
  384. }
  385. if (!query('GET_IMAGE_EDITOR_ALLOW_EDIT') || !query('GET_IMAGE_EDITOR_SUPPORT_EDIT'))
  386. return;
  387. // handle interactions
  388. root.ref.handleEdit = (e) => {
  389. e.stopPropagation();
  390. root.dispatch('EDIT_ITEM', { id });
  391. };
  392. updateEditButton(root, props);
  393. };
  394. const updateEditButton = (root, props) => {
  395. root.ref.buttonEditItem && root.removeChildView(root.ref.buttonEditItem);
  396. if (root.ref.editButton && root.ref.editButton.parentNode) {
  397. root.ref.editButton.parentNode.removeChild(root.ref.editButton);
  398. }
  399. if (hasPoster(root, props)) {
  400. // add edit button to preview
  401. const buttonView = view.createChildView(fileActionButton, {
  402. label: 'edit',
  403. icon: query('GET_IMAGE_EDITOR_ICON_EDIT'),
  404. opacity: 0,
  405. });
  406. // edit item classname
  407. buttonView.element.classList.add('filepond--action-edit-item');
  408. buttonView.element.dataset.align = query('GET_STYLE_IMAGE_EDITOR_BUTTON_EDIT_ITEM_POSITION');
  409. buttonView.on('click', root.ref.handleEdit);
  410. root.ref.buttonEditItem = view.appendChildView(buttonView);
  411. }
  412. else {
  413. // view is file info
  414. const filenameElement = view.element.querySelector('.filepond--file-info-main');
  415. const editButton = document.createElement('button');
  416. editButton.className = 'filepond--action-edit-item-alt';
  417. editButton.innerHTML = query('GET_IMAGE_EDITOR_ICON_EDIT') + '<span>edit</span>';
  418. editButton.addEventListener('click', root.ref.handleEdit);
  419. filenameElement.appendChild(editButton);
  420. root.ref.editButton = editButton;
  421. }
  422. };
  423. const didUpdateItemMetadata = ({ root, props, action }) => {
  424. if (!/poster/.test(action.change.key))
  425. return;
  426. if (!query('GET_IMAGE_EDITOR_ALLOW_EDIT') || !query('GET_IMAGE_EDITOR_SUPPORT_EDIT'))
  427. return;
  428. updateEditButton(root, props);
  429. };
  430. view.registerDestroyer(({ root }) => {
  431. if (root.ref.buttonEditItem) {
  432. root.ref.buttonEditItem.off('click', root.ref.handleEdit);
  433. }
  434. if (root.ref.editButton) {
  435. root.ref.editButton.removeEventListener('click', root.ref.handleEdit);
  436. }
  437. });
  438. const routes = {
  439. EDIT_ITEM: openImageEditor,
  440. DID_LOAD_ITEM: didLoadItem,
  441. DID_UPDATE_ITEM_METADATA: didUpdateItemMetadata,
  442. DID_REMOVE_ITEM: ({ props }) => {
  443. const { id } = props;
  444. const item = query('GET_ITEM', id);
  445. if (!item)
  446. return;
  447. const poster = item.getMetadata('poster');
  448. poster && URL.revokeObjectURL(poster);
  449. },
  450. REQUEST_CREATE_IMAGE_POSTER: createImagePoster,
  451. DID_FILE_POSTER_LOAD: undefined,
  452. };
  453. if (supportsFilePoster) {
  454. // view is file
  455. const didPosterUpdate = ({ root }) => {
  456. if (!root.ref.buttonEditItem)
  457. return;
  458. root.ref.buttonEditItem.opacity = 1;
  459. };
  460. routes.DID_FILE_POSTER_LOAD = didPosterUpdate;
  461. }
  462. //#endregion
  463. // start writing
  464. view.registerWriter(createRoute(routes));
  465. });
  466. //#region write image
  467. addFilter('SHOULD_PREPARE_OUTPUT', (shouldPrepareOutput, { query, change, item }) => new Promise((resolve) => {
  468. if (!query('GET_IMAGE_EDITOR_SUPPORT_IMAGE')(item.file))
  469. return resolve(false);
  470. if (change && !/imageState/.test(change.key))
  471. return resolve(false);
  472. resolve(!query('IS_ASYNC'));
  473. }));
  474. const shouldTransformFile = (query, file, item) => new Promise((resolve) => {
  475. // invalid item
  476. if (!couldTransformFile(query) ||
  477. item.archived ||
  478. (!isFile(file) && !isBlob(file)) ||
  479. !query('GET_IMAGE_EDITOR_SUPPORT_IMAGE')(file)) {
  480. return resolve(false);
  481. }
  482. // if size can't be read this browser doesn't support image
  483. getImageSize(file)
  484. .then(() => {
  485. const fn = query('GET_IMAGE_TRANSFORM_IMAGE_FILTER');
  486. if (fn) {
  487. const filterResult = fn(file);
  488. if (typeof filterResult === 'boolean') {
  489. return resolve(filterResult);
  490. }
  491. if (typeof filterResult.then === 'function') {
  492. return filterResult.then(resolve);
  493. }
  494. }
  495. resolve(true);
  496. })
  497. .catch(() => {
  498. resolve(false);
  499. });
  500. });
  501. // subscribe to file transformations
  502. addFilter('PREPARE_OUTPUT', (file, { query, item }) => {
  503. const writeOutputImage = (file) => new Promise((resolve, reject) => {
  504. const imageState = item.getMetadata('imageState');
  505. // no editor defined, then exit
  506. const { imageProcessor, imageReader, imageWriter, editorOptions, imageState: imageBaseState, } = getEditorSafe(query('GET_IMAGE_EDITOR'));
  507. if (!imageProcessor || !imageReader || !imageWriter || !editorOptions)
  508. return;
  509. const [createImageReader, imageReaderOptions] = imageReader;
  510. const [createImageWriter = noop, imageWriterOptions] = imageWriter;
  511. imageProcessor(file, {
  512. ...editorOptions,
  513. imageReader: createImageReader(imageReaderOptions),
  514. imageWriter: createImageWriter(imageWriterOptions),
  515. imageState: {
  516. ...imageBaseState,
  517. ...imageState,
  518. },
  519. })
  520. .then(resolve)
  521. .catch(reject);
  522. });
  523. return new Promise((resolve) => {
  524. shouldTransformFile(query, file, item).then((shouldWrite) => {
  525. if (!shouldWrite)
  526. return resolve(file);
  527. writeOutputImage(file).then((res) => {
  528. const afterFn = query('GET_IMAGE_EDITOR_AFTER_WRITE_IMAGE');
  529. if (afterFn)
  530. return afterFn(res).then(resolve);
  531. // @ts-ignore
  532. resolve(res.dest);
  533. });
  534. });
  535. });
  536. });
  537. //#endregion
  538. // Expose plugin options
  539. return {
  540. options: {
  541. // enable or disable image editing
  542. allowImageEditor: [true, Type.BOOLEAN],
  543. // open editor when image is dropped
  544. imageEditorInstantEdit: [false, Type.BOOLEAN],
  545. // allow editing
  546. imageEditorAllowEdit: [true, Type.BOOLEAN],
  547. // cannot edit if no WebGL or is <=IE11
  548. imageEditorSupportEdit: [
  549. isBrowser() && isModernBrowser() && supportsWebGL(),
  550. Type.BOOLEAN,
  551. ],
  552. // receives file and should return true if can edit
  553. imageEditorSupportImage: [isImage, Type.FUNCTION],
  554. // cannot write if is <= IE11
  555. imageEditorSupportWriteImage: [isModernBrowser(), Type.BOOLEAN],
  556. // should output image
  557. imageEditorWriteImage: [true, Type.BOOLEAN],
  558. // receives written image and can return single or more images
  559. imageEditorAfterWriteImage: [undefined, Type.FUNCTION],
  560. // editor object
  561. imageEditor: [null, Type.OBJECT],
  562. // the icon to use for the edit button
  563. imageEditorIconEdit: [
  564. '<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><path d="M8.5 17h1.586l7-7L15.5 8.414l-7 7V17zm-1.707-2.707l8-8a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-8 8A1 1 0 0 1 10.5 19h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707z" fill="currentColor" fill-rule="nonzero"/></svg>',
  565. Type.STRING,
  566. ],
  567. // location of processing button
  568. styleImageEditorButtonEditItemPosition: ['bottom center', Type.STRING],
  569. },
  570. };
  571. };
  572. // fire pluginloaded event if running in browser, this allows registering the plugin when using async script tags
  573. if (isBrowser())
  574. document.dispatchEvent(new CustomEvent('FilePond:pluginloaded', { detail: plugin }));
  575. export default plugin;